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/README.md
CHANGED
|
@@ -1,3 +1,271 @@
|
|
|
1
1
|
# Tesla Fleet API
|
|
2
|
-
A library for the [Tesla Fleet API](https://developer.tesla.com/docs/fleet-api), and the third parties services [Teslemetry](https://teslemetry.com/docs/getting-started/api) and [Tessie](https://developer.tessie.com/docs/tesla-api-comparison), which provide access to the Fleet API for their customers.
|
|
3
2
|
|
|
3
|
+
A TypeScript library for Tesla vehicles and energy sites. It reaches them through the [Tesla Fleet API](https://developer.tesla.com/docs/fleet-api), [Teslemetry](https://teslemetry.com/docs/getting-started/api), [Tessie](https://developer.tessie.com/docs/tesla-api-comparison), or directly over Bluetooth. It is the Node counterpart of [`python-tesla-fleet-api`](https://github.com/Teslemetry/python-tesla-fleet-api), with the same method names in `snake_case`, so docs and router code line up across the two.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install tesla-fleet-api
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It needs Node 22 or later, ships ESM and CommonJS, and has one runtime dependency, which is pure JavaScript. Nothing native is installed.
|
|
10
|
+
|
|
11
|
+
> **Not yet proven on hardware.** Vehicle Bluetooth and local Powerwall are tested against independent fakes in this repository, and no Node code has run them on a real car, Homey, proxy or gateway. [OBSERVATIONS.md](OBSERVATIONS.md) records what has been measured.
|
|
12
|
+
|
|
13
|
+
## What is in the package
|
|
14
|
+
|
|
15
|
+
| Client | Reaches | Vehicles | Energy sites |
|
|
16
|
+
| -- | -- | -- | -- |
|
|
17
|
+
| `TeslaFleetApi` | Tesla Fleet API | `vehicles.createFleet(vin)`, `vehicles.createSigned(vin, key?)` | `energySites.create(id, key?, din?)` |
|
|
18
|
+
| `TeslaFleetOAuth` | Fleet API, with its own token refresh | As `TeslaFleetApi` | As `TeslaFleetApi` |
|
|
19
|
+
| `Teslemetry` | Teslemetry | `vehicles.create(vin)` | `energySites.create(id, key?, din?)` |
|
|
20
|
+
| `Tessie` | Tessie | `vehicles.create(vin)` | None |
|
|
21
|
+
| `TeslaBluetooth` | The vehicle, over Bluetooth | `vehicles.createBluetooth(vin, options)` | None |
|
|
22
|
+
|
|
23
|
+
| Import | Contents |
|
|
24
|
+
| -- | -- |
|
|
25
|
+
| `tesla-fleet-api` | The cloud clients, errors, constants, tariff, firmware helpers, funnel, `jws` |
|
|
26
|
+
| `tesla-fleet-api/router` | `VehicleRouter`, `EnergySiteRouter`, merge helpers, `is_failover_safe` |
|
|
27
|
+
| `tesla-fleet-api/bluetooth` | `TeslaBluetooth`, `VehicleBluetooth`, the `BleTransport` contract, stream glue |
|
|
28
|
+
| `tesla-fleet-api/bluetooth/homey` | Bluetooth through a Homey app |
|
|
29
|
+
| `tesla-fleet-api/bluetooth/noble` | Bluetooth through `@stoprocent/noble` |
|
|
30
|
+
| `tesla-fleet-api/tariff` | `getTariffPeriods`, `unwrapTariffV2`, with no protobuf code |
|
|
31
|
+
| `tesla-fleet-api/const` | Enums, `Models`, `Region`, `SERVERS` |
|
|
32
|
+
| `tesla-fleet-api/types` | Response types |
|
|
33
|
+
|
|
34
|
+
Deep imports such as `tesla-fleet-api/dist/...` are blocked by the `exports` map.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { TeslaFleetApi } from "tesla-fleet-api";
|
|
40
|
+
|
|
41
|
+
const api = new TeslaFleetApi({ accessToken, region: "na" });
|
|
42
|
+
|
|
43
|
+
const { response } = await api.products();
|
|
44
|
+
const car = api.vehicles!.createFleet(vin);
|
|
45
|
+
await car.wake_up({ wait: true });
|
|
46
|
+
await car.vehicle_data();
|
|
47
|
+
|
|
48
|
+
const site = api.energySites!.create(energySiteId);
|
|
49
|
+
await site.live_status();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Replies keep the service's `{ response }` envelope. `api.vehicles.byVin` and `api.energySites.byId` are `Map`s of what each factory created. More: [vehicles](docs/fleet_api_vehicles.md), [energy sites](docs/fleet_api_energy_sites.md).
|
|
53
|
+
|
|
54
|
+
`accessToken` may be a function returning `Promise<string | null>`, which is called for every request. Other options are `fetch`, `logger`, `debug` and `timeout` (milliseconds).
|
|
55
|
+
|
|
56
|
+
## Signed commands
|
|
57
|
+
|
|
58
|
+
A vehicle that requires the Vehicle Command Protocol refuses plaintext commands. `createSigned` builds each one locally and sends it through `signed_command`. The key is the raw 32-byte P-256 private key registered with the car.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { TeslaFleetApi } from "tesla-fleet-api";
|
|
62
|
+
|
|
63
|
+
const api = new TeslaFleetApi({ accessToken, region: "na" });
|
|
64
|
+
const privateKey = await api.get_private_key("private_key.pem");
|
|
65
|
+
|
|
66
|
+
const signed = api.vehicles!.createSigned(vin, privateKey);
|
|
67
|
+
await signed.door_lock();
|
|
68
|
+
await signed.set_charge_limit(80);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A refusal by the vehicle resolves as `{ response: { result: false, reason } }`. It is not an error. Passing `false` as the key disables signing, and leaving it out falls back to the client's `private_key`. See [signed commands](docs/fleet_api_signed_commands.md).
|
|
72
|
+
|
|
73
|
+
## Energy sites
|
|
74
|
+
|
|
75
|
+
Device commands are signed with an RSA key, so create the site with the key and the gateway DIN to send them. A site created without a key sends the unsigned form, except the commands that must never go unsigned: `set_island_mode`, `go_off_grid` and `reconnect_grid` raise `SignedCommandRequired`, because gateways acknowledge the unsigned command without moving the contactor.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { TeslaFleetApi } from "tesla-fleet-api";
|
|
79
|
+
|
|
80
|
+
const api = new TeslaFleetApi({ accessToken, region: "na" });
|
|
81
|
+
const rsaKey = await api.get_rsa_private_key("tedapi_rsa_private.pem");
|
|
82
|
+
|
|
83
|
+
const site = api.energySites!.create(energySiteId, rsaKey, din);
|
|
84
|
+
await site.get_system_info();
|
|
85
|
+
await site.go_off_grid();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`add_authorized_client` registers a key with a gateway, and `TeslemetryEnergySite.wait_until_paired` waits for the owner's breaker toggle. A signed send is never retried, because the same bytes would execute again.
|
|
89
|
+
|
|
90
|
+
## Teslemetry and Tessie
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { ClosureState, Teslemetry, Tessie } from "tesla-fleet-api";
|
|
94
|
+
|
|
95
|
+
const teslemetry = new Teslemetry(accessToken);
|
|
96
|
+
await teslemetry.ping();
|
|
97
|
+
const car = teslemetry.vehicles.create(vin);
|
|
98
|
+
await car.closure({ front_trunk: ClosureState.OPEN });
|
|
99
|
+
const site = teslemetry.energySites!.create(energySiteId);
|
|
100
|
+
await site.find_authorized_clients();
|
|
101
|
+
|
|
102
|
+
const tessie = new Tessie(accessToken, { wait_for_completion: true, max_attempts: 3 });
|
|
103
|
+
const tessieCar = tessie.vehicles.create(vin);
|
|
104
|
+
await tessieCar.lock();
|
|
105
|
+
await tessieCar.drives({ limit: 10 });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Both clients expose `vehicles.create` only: `createFleet` and `createSigned` are compile errors, because these services do not take Fleet API credentials. See [Teslemetry](docs/teslemetry.md) and [Tessie](docs/tessie.md).
|
|
109
|
+
|
|
110
|
+
## Routers
|
|
111
|
+
|
|
112
|
+
A router tries one backend after another, per command. It falls over only on a failure that proves the command did not reach the car or gateway. A refusal by the device is returned, and a Bluetooth command with an unresolved outcome is never replayed.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { TeslaFleetApi } from "tesla-fleet-api";
|
|
116
|
+
import { TeslaBluetooth } from "tesla-fleet-api/bluetooth";
|
|
117
|
+
import { NobleBleTransport } from "tesla-fleet-api/bluetooth/noble";
|
|
118
|
+
import { VehicleRouter } from "tesla-fleet-api/router";
|
|
119
|
+
|
|
120
|
+
const ble = new TeslaBluetooth({ privateKey }).vehicles.createBluetooth(vin, { transport: new NobleBleTransport() });
|
|
121
|
+
const cloud = new TeslaFleetApi({ accessToken, region: "na" }).vehicles!.createSigned(vin, privateKey);
|
|
122
|
+
|
|
123
|
+
const router = new VehicleRouter([ble, cloud]);
|
|
124
|
+
await router.door_lock();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Methods and their types come from the backends. A misspelt method is a compile error, and a method only one backend has goes to that backend. `EnergySiteRouter` does the same for sites, and `merge_live_status` and `merge_site_info` overlay local readings on cloud documents. See [routers](docs/routers.md).
|
|
128
|
+
|
|
129
|
+
## Bluetooth
|
|
130
|
+
|
|
131
|
+
Install the backend you want, and pass its transport to `createBluetooth`. Each adapter is its own entry point, and importing one loads no backend.
|
|
132
|
+
|
|
133
|
+
| Adapter | Class | Install | Runs on |
|
|
134
|
+
| -- | -- | -- | -- |
|
|
135
|
+
| `./bluetooth/homey` | `HomeyBleTransport` | Nothing. It takes `this.homey.ble`. | A Homey app |
|
|
136
|
+
| `./bluetooth/noble` | `NobleBleTransport` | `@stoprocent/noble`, which is native | Linux, macOS, Windows |
|
|
137
|
+
|
|
138
|
+
On a computer:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { TeslaBluetooth } from "tesla-fleet-api/bluetooth";
|
|
142
|
+
import { NobleBleTransport } from "tesla-fleet-api/bluetooth/noble";
|
|
143
|
+
|
|
144
|
+
const tesla = new TeslaBluetooth({ privateKey });
|
|
145
|
+
const vehicle = tesla.vehicles.createBluetooth(vin, { transport: new NobleBleTransport() });
|
|
146
|
+
|
|
147
|
+
await vehicle.wake_up({ wait: true });
|
|
148
|
+
await vehicle.door_lock();
|
|
149
|
+
await vehicle.charge_state();
|
|
150
|
+
await vehicle.disconnect();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
In a Homey app, which needs the `homey:wireless:ble` permission:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import Homey from "homey";
|
|
157
|
+
import { TeslaBluetooth } from "tesla-fleet-api/bluetooth";
|
|
158
|
+
import { HOMEY_VEHICLE_OPTIONS, HomeyBleTransport } from "tesla-fleet-api/bluetooth/homey";
|
|
159
|
+
|
|
160
|
+
class TeslaApp extends Homey.App {
|
|
161
|
+
async lock(vin: string, privateKey: Buffer): Promise<void> {
|
|
162
|
+
const transport = new HomeyBleTransport(this.homey.ble);
|
|
163
|
+
const vehicle = new TeslaBluetooth({ privateKey }).vehicles.createBluetooth(vin, {
|
|
164
|
+
transport,
|
|
165
|
+
...HOMEY_VEHICLE_OPTIONS, // one 4 s attempt, for a Flow action's 9 s window
|
|
166
|
+
});
|
|
167
|
+
await vehicle.door_lock();
|
|
168
|
+
await vehicle.disconnect();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default TeslaApp;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The Homey adapter's defaults come from a BLE-00 run on a real Homey (one write per frame, a 15 s scan bound). `confirmation` sets how much proof a command needs: `"optimistic"`, `"ack"` (the default) or `"verify"`. New keys are enrolled with `vehicle.pair()`. [docs/bluetooth_vehicles.md](docs/bluetooth_vehicles.md) has every option and each adapter's limits.
|
|
176
|
+
|
|
177
|
+
## Local Powerwall
|
|
178
|
+
|
|
179
|
+
A client for a Powerwall gateway on your LAN is exported as `tesla-fleet-api/powerwall`. A read-only check has passed on a real gateway, but no write has run from Node on real hardware. [docs/powerwall.md](docs/powerwall.md) describes the API.
|
|
180
|
+
|
|
181
|
+
## OAuth
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { Scope, TeslaFleetOAuth } from "tesla-fleet-api";
|
|
185
|
+
|
|
186
|
+
const api = new TeslaFleetOAuth({ region: "na", client_id, client_secret, redirect_uri });
|
|
187
|
+
const url = api.get_login_url([Scope.OPENID, Scope.OFFLINE_ACCESS]);
|
|
188
|
+
await api.get_refresh_token(code);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
An expired token refreshes before the next request. Tesla's refresh tokens are single use, so store `api.refresh_token` and `api.expires` (epoch seconds) after `get_refresh_token`, and whatever `refresh_access_token()` or `check_access_token()` returns. To store a refresh made by a request, read those two fields after it.
|
|
192
|
+
|
|
193
|
+
## Keys
|
|
194
|
+
|
|
195
|
+
`get_private_key` and `get_rsa_private_key` load a PEM file, or create it at mode 0600. Python's library reads the same files.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { TeslaFleetApi } from "tesla-fleet-api";
|
|
199
|
+
|
|
200
|
+
const api = new TeslaFleetApi({ accessToken, region: "na" });
|
|
201
|
+
|
|
202
|
+
await api.get_private_key("private_key.pem"); // raw 32-byte Buffer
|
|
203
|
+
api.vehicles!.createSigned(vin); // falls back to it
|
|
204
|
+
console.log(api.public_pem);
|
|
205
|
+
|
|
206
|
+
await api.get_rsa_private_key("tedapi_rsa_private.pem"); // RSA KeyObject
|
|
207
|
+
console.log(api.rsa_public_der_pkcs1_b64);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
To keep keys somewhere other than a file, as Homey does:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import {
|
|
214
|
+
TeslaFleetApi,
|
|
215
|
+
ec_private_key_from_pem,
|
|
216
|
+
generate_ec_private_key_pem,
|
|
217
|
+
generate_rsa_private_key_pem,
|
|
218
|
+
rsa_public_der_pkcs1,
|
|
219
|
+
} from "tesla-fleet-api";
|
|
220
|
+
|
|
221
|
+
const api = new TeslaFleetApi({ accessToken, region: "na" });
|
|
222
|
+
const pem = await generate_ec_private_key_pem();
|
|
223
|
+
api.private_key = ec_private_key_from_pem(pem);
|
|
224
|
+
|
|
225
|
+
const rsaPem = await generate_rsa_private_key_pem();
|
|
226
|
+
console.log(rsa_public_der_pkcs1(rsaPem));
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
An existing file or PEM that cannot be used raises `PrivateKeyError`, whose `reason` is `unreadable`, `encrypted`, `malformed` or `wrong_type`.
|
|
230
|
+
|
|
231
|
+
## Errors
|
|
232
|
+
|
|
233
|
+
Every failure rejects with a real `Error` subclass, named as in Python's `exceptions.py`, so `except X` there is `instanceof X` here.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { RateLimited, TeslaFleetApi, TeslaFleetError } from "tesla-fleet-api";
|
|
237
|
+
|
|
238
|
+
const api = new TeslaFleetApi({ accessToken, region: "na" });
|
|
239
|
+
try {
|
|
240
|
+
await api.products();
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error instanceof RateLimited) console.log(error.data);
|
|
243
|
+
else if (error instanceof TeslaFleetError) console.log(error.status, error.key);
|
|
244
|
+
else throw error;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Everything else
|
|
249
|
+
|
|
250
|
+
- `getTariffPeriods(tariff, now, { timeZone })` resolves Tariff V2 rates, and `unwrapTariffV2` finds the tariff in a `site_info` reply. A tariff carries no time zone, so `timeZone` is required.
|
|
251
|
+
- `firmwareCompare` and `firmwareAtLeast` compare firmware versions numerically, because `"2025.10" < "2025.9"` as strings.
|
|
252
|
+
- `ObservationFunnel` feeds one listener per field from many sources, so a value survives the loss of any one of them. `VehicleDataResultPublisher` and `TeslemetryStreamPublisher` publish into it, and `BleBroadcastPublisher` does from `./bluetooth`.
|
|
253
|
+
- `jws` holds the Tesla.SS256 signatures behind signed Fleet Telemetry configuration.
|
|
254
|
+
|
|
255
|
+
The classes a factory creates are static fields, so a subclass swaps them without redefining a constructor:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { Teslemetry, Vehicle, VehicleSpecific } from "tesla-fleet-api";
|
|
259
|
+
|
|
260
|
+
class MyVehicleSpecific extends VehicleSpecific {}
|
|
261
|
+
class MyVehicles extends Vehicle {
|
|
262
|
+
static override Fleet = MyVehicleSpecific;
|
|
263
|
+
}
|
|
264
|
+
class MyApi extends Teslemetry {
|
|
265
|
+
static override Vehicles = MyVehicles;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export { MyApi };
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Behaviour that differs from Python on purpose is listed by `pnpm parity --python <clone>`, which compares this package with Python's public surface.
|
|
@@ -5,6 +5,15 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __exportAll = (all, no_symbols) => {
|
|
9
|
+
let target = {};
|
|
10
|
+
for (var name in all) __defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true
|
|
13
|
+
});
|
|
14
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
15
|
+
return target;
|
|
16
|
+
};
|
|
8
17
|
var __copyProps = (to, from, except, desc) => {
|
|
9
18
|
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
19
|
key = keys[i];
|
|
@@ -19,4 +28,5 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
19
28
|
value: mod,
|
|
20
29
|
enumerable: true
|
|
21
30
|
}) : target, mod));
|
|
31
|
+
exports.__exportAll = __exportAll;
|
|
22
32
|
exports.__toESM = __toESM;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/bluetooth/adapters/shared.ts
|
|
2
|
+
/** Lower case with no dashes, and short for a Bluetooth SIG UUID: the form Homey and noble use. */
|
|
3
|
+
function compactUuid(uuid) {
|
|
4
|
+
const compact = uuid.toLowerCase().replace(/-/g, "");
|
|
5
|
+
return /^0000([0-9a-f]{4})00001000800000805f9b34fb$/.exec(compact)?.[1] ?? compact;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Which connect attempt owns each device's radio link. The library abandons a
|
|
9
|
+
* hung attempt and closes it if it lands later, but both attempts reach the same
|
|
10
|
+
* link: Homey's `connect()` returns the existing one, noble reuses the peripheral,
|
|
11
|
+
* and a proxy keys links by address. So a superseded attempt must leave the link
|
|
12
|
+
* alone, or its close would kill the attempt that replaced it.
|
|
13
|
+
*/
|
|
14
|
+
var LinkClaims = class {
|
|
15
|
+
owners = /* @__PURE__ */ new Map();
|
|
16
|
+
/** This attempt now owns the link. */
|
|
17
|
+
claim(id, attempt) {
|
|
18
|
+
this.owners.set(id, attempt);
|
|
19
|
+
}
|
|
20
|
+
/** Whether a newer attempt owns the link. */
|
|
21
|
+
superseded(id, attempt) {
|
|
22
|
+
const owner = this.owners.get(id);
|
|
23
|
+
return owner !== void 0 && owner !== attempt;
|
|
24
|
+
}
|
|
25
|
+
/** The attempt is done. If it owned the link, no one does until the next claim. */
|
|
26
|
+
release(id, attempt) {
|
|
27
|
+
if (this.owners.get(id) === attempt) this.owners.delete(id);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** Run a cleanup whose failure must not hide the failure behind it. */
|
|
31
|
+
async function quietly(cleanup) {
|
|
32
|
+
try {
|
|
33
|
+
await cleanup();
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
exports.LinkClaims = LinkClaims;
|
|
37
|
+
exports.compactUuid = compactUuid;
|
|
38
|
+
exports.quietly = quietly;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/bluetooth/adapters/shared.ts
|
|
2
|
+
/** Lower case with no dashes, and short for a Bluetooth SIG UUID: the form Homey and noble use. */
|
|
3
|
+
function compactUuid(uuid) {
|
|
4
|
+
const compact = uuid.toLowerCase().replace(/-/g, "");
|
|
5
|
+
return /^0000([0-9a-f]{4})00001000800000805f9b34fb$/.exec(compact)?.[1] ?? compact;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Which connect attempt owns each device's radio link. The library abandons a
|
|
9
|
+
* hung attempt and closes it if it lands later, but both attempts reach the same
|
|
10
|
+
* link: Homey's `connect()` returns the existing one, noble reuses the peripheral,
|
|
11
|
+
* and a proxy keys links by address. So a superseded attempt must leave the link
|
|
12
|
+
* alone, or its close would kill the attempt that replaced it.
|
|
13
|
+
*/
|
|
14
|
+
var LinkClaims = class {
|
|
15
|
+
owners = /* @__PURE__ */ new Map();
|
|
16
|
+
/** This attempt now owns the link. */
|
|
17
|
+
claim(id, attempt) {
|
|
18
|
+
this.owners.set(id, attempt);
|
|
19
|
+
}
|
|
20
|
+
/** Whether a newer attempt owns the link. */
|
|
21
|
+
superseded(id, attempt) {
|
|
22
|
+
const owner = this.owners.get(id);
|
|
23
|
+
return owner !== void 0 && owner !== attempt;
|
|
24
|
+
}
|
|
25
|
+
/** The attempt is done. If it owned the link, no one does until the next claim. */
|
|
26
|
+
release(id, attempt) {
|
|
27
|
+
if (this.owners.get(id) === attempt) this.owners.delete(id);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** Run a cleanup whose failure must not hide the failure behind it. */
|
|
31
|
+
async function quietly(cleanup) {
|
|
32
|
+
try {
|
|
33
|
+
await cleanup();
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
export { LinkClaims, compactUuid, quietly };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/bluetooth/async.ts
|
|
2
|
+
/** A bound on a backend call ran out. The call itself may still finish, or never. */
|
|
3
|
+
var BleCallTimeout = class extends Error {
|
|
4
|
+
name = "BleCallTimeout";
|
|
5
|
+
};
|
|
6
|
+
/** A first-come, first-served mutex. Not reentrant. */
|
|
7
|
+
var AsyncLock = class {
|
|
8
|
+
tail = Promise.resolve();
|
|
9
|
+
async run(fn) {
|
|
10
|
+
const previous = this.tail;
|
|
11
|
+
let release;
|
|
12
|
+
this.tail = new Promise((resolve) => {
|
|
13
|
+
release = resolve;
|
|
14
|
+
});
|
|
15
|
+
await previous;
|
|
16
|
+
try {
|
|
17
|
+
return await fn();
|
|
18
|
+
} finally {
|
|
19
|
+
release();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Settle with `operation`, or reject with `BleCallTimeout` after `timeoutMs`.
|
|
25
|
+
* Backends may hang, so nothing here waits on them for longer than that.
|
|
26
|
+
* @param signal Rejects early with the signal's reason, and leaves no timer behind.
|
|
27
|
+
*/
|
|
28
|
+
function withTimeout(operation, timeoutMs, signal) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const stop = () => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
signal?.removeEventListener("abort", onAbort);
|
|
33
|
+
};
|
|
34
|
+
const onAbort = () => {
|
|
35
|
+
stop();
|
|
36
|
+
reject(signal?.reason);
|
|
37
|
+
};
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
stop();
|
|
40
|
+
reject(new BleCallTimeout(`Bluetooth call timed out after ${timeoutMs} ms.`));
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
43
|
+
(async () => operation())().then((value) => {
|
|
44
|
+
stop();
|
|
45
|
+
resolve(value);
|
|
46
|
+
}, (error) => {
|
|
47
|
+
stop();
|
|
48
|
+
reject(error);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Resolve after `ms`, or as soon as `signal` aborts. */
|
|
53
|
+
function sleep(ms, signal) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
if (signal?.aborted) {
|
|
56
|
+
resolve();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const finish = () => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
signal?.removeEventListener("abort", finish);
|
|
62
|
+
resolve();
|
|
63
|
+
};
|
|
64
|
+
const timer = setTimeout(finish, ms);
|
|
65
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Settle with `operation`, or reject with the signal's reason as soon as it aborts. */
|
|
69
|
+
function abortable(operation, signal) {
|
|
70
|
+
if (!signal) return operation;
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const onAbort = () => reject(signal.reason);
|
|
73
|
+
if (signal.aborted) onAbort();
|
|
74
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
75
|
+
const stop = () => signal.removeEventListener("abort", onAbort);
|
|
76
|
+
operation.then((value) => {
|
|
77
|
+
stop();
|
|
78
|
+
resolve(value);
|
|
79
|
+
}, (error) => {
|
|
80
|
+
stop();
|
|
81
|
+
reject(error);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** A queue with one consumer at a time. */
|
|
86
|
+
var ReplyQueue = class {
|
|
87
|
+
items = [];
|
|
88
|
+
waiter = null;
|
|
89
|
+
push(item) {
|
|
90
|
+
if (this.waiter) this.waiter(item);
|
|
91
|
+
else this.items.push(item);
|
|
92
|
+
}
|
|
93
|
+
/** Empty the queue and return what was in it. */
|
|
94
|
+
drain() {
|
|
95
|
+
return this.items.splice(0);
|
|
96
|
+
}
|
|
97
|
+
/** The next item, or `null` once `timeoutMs` passes or `signal` aborts. */
|
|
98
|
+
get(timeoutMs, signal) {
|
|
99
|
+
const queued = this.items.shift();
|
|
100
|
+
if (queued !== void 0) return Promise.resolve(queued);
|
|
101
|
+
if (timeoutMs <= 0 || signal.aborted) return Promise.resolve(null);
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const finish = (item) => {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
signal.removeEventListener("abort", onAbort);
|
|
106
|
+
this.waiter = null;
|
|
107
|
+
resolve(item);
|
|
108
|
+
};
|
|
109
|
+
const onAbort = () => finish(null);
|
|
110
|
+
const timer = setTimeout(onAbort, timeoutMs);
|
|
111
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
112
|
+
this.waiter = finish;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
exports.AsyncLock = AsyncLock;
|
|
117
|
+
exports.BleCallTimeout = BleCallTimeout;
|
|
118
|
+
exports.ReplyQueue = ReplyQueue;
|
|
119
|
+
exports.abortable = abortable;
|
|
120
|
+
exports.sleep = sleep;
|
|
121
|
+
exports.withTimeout = withTimeout;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A queue with one consumer at a time. */
|
|
2
|
+
export declare class ReplyQueue<T> {
|
|
3
|
+
private items;
|
|
4
|
+
private waiter;
|
|
5
|
+
push(item: T): void;
|
|
6
|
+
/** Empty the queue and return what was in it. */
|
|
7
|
+
drain(): T[];
|
|
8
|
+
/** The next item, or `null` once `timeoutMs` passes or `signal` aborts. */
|
|
9
|
+
get(timeoutMs: number, signal: AbortSignal): Promise<T | null>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A queue with one consumer at a time. */
|
|
2
|
+
export declare class ReplyQueue<T> {
|
|
3
|
+
private items;
|
|
4
|
+
private waiter;
|
|
5
|
+
push(item: T): void;
|
|
6
|
+
/** Empty the queue and return what was in it. */
|
|
7
|
+
drain(): T[];
|
|
8
|
+
/** The next item, or `null` once `timeoutMs` passes or `signal` aborts. */
|
|
9
|
+
get(timeoutMs: number, signal: AbortSignal): Promise<T | null>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/bluetooth/async.ts
|
|
2
|
+
/** A bound on a backend call ran out. The call itself may still finish, or never. */
|
|
3
|
+
var BleCallTimeout = class extends Error {
|
|
4
|
+
name = "BleCallTimeout";
|
|
5
|
+
};
|
|
6
|
+
/** A first-come, first-served mutex. Not reentrant. */
|
|
7
|
+
var AsyncLock = class {
|
|
8
|
+
tail = Promise.resolve();
|
|
9
|
+
async run(fn) {
|
|
10
|
+
const previous = this.tail;
|
|
11
|
+
let release;
|
|
12
|
+
this.tail = new Promise((resolve) => {
|
|
13
|
+
release = resolve;
|
|
14
|
+
});
|
|
15
|
+
await previous;
|
|
16
|
+
try {
|
|
17
|
+
return await fn();
|
|
18
|
+
} finally {
|
|
19
|
+
release();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Settle with `operation`, or reject with `BleCallTimeout` after `timeoutMs`.
|
|
25
|
+
* Backends may hang, so nothing here waits on them for longer than that.
|
|
26
|
+
* @param signal Rejects early with the signal's reason, and leaves no timer behind.
|
|
27
|
+
*/
|
|
28
|
+
function withTimeout(operation, timeoutMs, signal) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const stop = () => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
signal?.removeEventListener("abort", onAbort);
|
|
33
|
+
};
|
|
34
|
+
const onAbort = () => {
|
|
35
|
+
stop();
|
|
36
|
+
reject(signal?.reason);
|
|
37
|
+
};
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
stop();
|
|
40
|
+
reject(new BleCallTimeout(`Bluetooth call timed out after ${timeoutMs} ms.`));
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
43
|
+
(async () => operation())().then((value) => {
|
|
44
|
+
stop();
|
|
45
|
+
resolve(value);
|
|
46
|
+
}, (error) => {
|
|
47
|
+
stop();
|
|
48
|
+
reject(error);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Resolve after `ms`, or as soon as `signal` aborts. */
|
|
53
|
+
function sleep(ms, signal) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
if (signal?.aborted) {
|
|
56
|
+
resolve();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const finish = () => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
signal?.removeEventListener("abort", finish);
|
|
62
|
+
resolve();
|
|
63
|
+
};
|
|
64
|
+
const timer = setTimeout(finish, ms);
|
|
65
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Settle with `operation`, or reject with the signal's reason as soon as it aborts. */
|
|
69
|
+
function abortable(operation, signal) {
|
|
70
|
+
if (!signal) return operation;
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const onAbort = () => reject(signal.reason);
|
|
73
|
+
if (signal.aborted) onAbort();
|
|
74
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
75
|
+
const stop = () => signal.removeEventListener("abort", onAbort);
|
|
76
|
+
operation.then((value) => {
|
|
77
|
+
stop();
|
|
78
|
+
resolve(value);
|
|
79
|
+
}, (error) => {
|
|
80
|
+
stop();
|
|
81
|
+
reject(error);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** A queue with one consumer at a time. */
|
|
86
|
+
var ReplyQueue = class {
|
|
87
|
+
items = [];
|
|
88
|
+
waiter = null;
|
|
89
|
+
push(item) {
|
|
90
|
+
if (this.waiter) this.waiter(item);
|
|
91
|
+
else this.items.push(item);
|
|
92
|
+
}
|
|
93
|
+
/** Empty the queue and return what was in it. */
|
|
94
|
+
drain() {
|
|
95
|
+
return this.items.splice(0);
|
|
96
|
+
}
|
|
97
|
+
/** The next item, or `null` once `timeoutMs` passes or `signal` aborts. */
|
|
98
|
+
get(timeoutMs, signal) {
|
|
99
|
+
const queued = this.items.shift();
|
|
100
|
+
if (queued !== void 0) return Promise.resolve(queued);
|
|
101
|
+
if (timeoutMs <= 0 || signal.aborted) return Promise.resolve(null);
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const finish = (item) => {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
signal.removeEventListener("abort", onAbort);
|
|
106
|
+
this.waiter = null;
|
|
107
|
+
resolve(item);
|
|
108
|
+
};
|
|
109
|
+
const onAbort = () => finish(null);
|
|
110
|
+
const timer = setTimeout(onAbort, timeoutMs);
|
|
111
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
112
|
+
this.waiter = finish;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
export { AsyncLock, BleCallTimeout, ReplyQueue, abortable, sleep, withTimeout };
|