tesla-fleet-api 0.5.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 CHANGED
@@ -27,7 +27,6 @@ It needs Node 22 or later, ships ESM and CommonJS, and has one runtime dependenc
27
27
  | `tesla-fleet-api/bluetooth` | `TeslaBluetooth`, `VehicleBluetooth`, the `BleTransport` contract, stream glue |
28
28
  | `tesla-fleet-api/bluetooth/homey` | Bluetooth through a Homey app |
29
29
  | `tesla-fleet-api/bluetooth/noble` | Bluetooth through `@stoprocent/noble` |
30
- | `tesla-fleet-api/bluetooth/esphome` | Bluetooth through an ESPHome proxy |
31
30
  | `tesla-fleet-api/tariff` | `getTariffPeriods`, `unwrapTariffV2`, with no protobuf code |
32
31
  | `tesla-fleet-api/const` | Enums, `Models`, `Region`, `SERVERS` |
33
32
  | `tesla-fleet-api/types` | Response types |
@@ -135,7 +134,6 @@ Install the backend you want, and pass its transport to `createBluetooth`. Each
135
134
  | -- | -- | -- | -- |
136
135
  | `./bluetooth/homey` | `HomeyBleTransport` | Nothing. It takes `this.homey.ble`. | A Homey app |
137
136
  | `./bluetooth/noble` | `NobleBleTransport` | `@stoprocent/noble`, which is native | Linux, macOS, Windows |
138
- | `./bluetooth/esphome` | `EsphomeBleTransport` | `@2colors/esphome-native-api` | Anything with a TCP route to a dedicated proxy |
139
137
 
140
138
  On a computer:
141
139
 
@@ -152,19 +150,6 @@ await vehicle.charge_state();
152
150
  await vehicle.disconnect();
153
151
  ```
154
152
 
155
- Through an ESPHome proxy:
156
-
157
- ```ts
158
- import { TeslaBluetooth } from "tesla-fleet-api/bluetooth";
159
- import { EsphomeBleTransport } from "tesla-fleet-api/bluetooth/esphome";
160
-
161
- const transport = new EsphomeBleTransport({ host: proxyHost, encryptionKey });
162
- const vehicle = new TeslaBluetooth({ privateKey }).vehicles.createBluetooth(vin, { transport });
163
- await vehicle.door_lock();
164
- await vehicle.disconnect();
165
- transport.close();
166
- ```
167
-
168
153
  In a Homey app, which needs the `homey:wireless:ble` permission:
169
154
 
170
155
  ```ts
@@ -187,7 +172,7 @@ class TeslaApp extends Homey.App {
187
172
  export default TeslaApp;
188
173
  ```
189
174
 
190
- The Homey adapter's defaults are conservative guesses until the BLE-00 probe has run on a Homey. `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.
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.
191
176
 
192
177
  ## Local Powerwall
193
178
 
@@ -17,8 +17,6 @@ const NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb";
17
17
  const APPEARANCE_UUID = "00002a01-0000-1000-8000-00805f9b34fb";
18
18
  /** Generic Access, for a backend that needs a service UUID to reach the two above. */
19
19
  const GENERIC_ACCESS_UUID = "00001800-0000-1000-8000-00805f9b34fb";
20
- /** Client Characteristic Configuration. Only an ESPHome proxy needs it written by hand. */
21
- const CCCD_UUID = "00002902-0000-1000-8000-00805f9b34fb";
22
20
  /** Largest frame accepted, length prefix included. Tesla's Go SDK allows 2 more bytes. */
23
21
  const MAX_FRAME_SIZE = 1024;
24
22
  /**
@@ -56,7 +54,7 @@ const DEFAULT_PAIR_POLL_INTERVAL_MS = 5e3;
56
54
  const DEFAULT_CONNECT_ATTEMPTS = 3;
57
55
  /** One connect attempt. Python takes the same figure from `bleak-retry-connector`. */
58
56
  const DEFAULT_CONNECT_TIMEOUT_MS = 2e4;
59
- /** One scan. It must outlast the backend's window, which Homey fixes at about 5 s. */
57
+ /** One scan. It must outlast the backend's window, which Homey fixes at about 10 s: its adapter sets its own bound. */
60
58
  const DEFAULT_SCAN_TIMEOUT_MS = 1e4;
61
59
  /** Bound on tearing down a link: the failure behind it is what gets reported. */
62
60
  const LINK_TEARDOWN_TIMEOUT_MS = 5e3;
@@ -70,7 +68,6 @@ const GATT_WRITE_TIMEOUT_MS = 5e3;
70
68
  /** One read call, other than a keepalive's. */
71
69
  const GATT_READ_TIMEOUT_MS = 5e3;
72
70
  exports.APPEARANCE_UUID = APPEARANCE_UUID;
73
- exports.CCCD_UUID = CCCD_UUID;
74
71
  exports.CHARGE_PORT_VERIFY_INTERVAL_MS = CHARGE_PORT_VERIFY_INTERVAL_MS;
75
72
  exports.CHARGE_PORT_VERIFY_TIMEOUT_MS = CHARGE_PORT_VERIFY_TIMEOUT_MS;
76
73
  exports.DEFAULT_CONNECT_ATTEMPTS = DEFAULT_CONNECT_ATTEMPTS;
@@ -16,8 +16,6 @@ export declare const NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb";
16
16
  export declare const APPEARANCE_UUID = "00002a01-0000-1000-8000-00805f9b34fb";
17
17
  /** Generic Access, for a backend that needs a service UUID to reach the two above. */
18
18
  export declare const GENERIC_ACCESS_UUID = "00001800-0000-1000-8000-00805f9b34fb";
19
- /** Client Characteristic Configuration. Only an ESPHome proxy needs it written by hand. */
20
- export declare const CCCD_UUID = "00002902-0000-1000-8000-00805f9b34fb";
21
19
  /** Largest frame accepted, length prefix included. Tesla's Go SDK allows 2 more bytes. */
22
20
  export declare const MAX_FRAME_SIZE = 1024;
23
21
  /**
@@ -55,7 +53,7 @@ export declare const DEFAULT_PAIR_POLL_INTERVAL_MS = 5000;
55
53
  export declare const DEFAULT_CONNECT_ATTEMPTS = 3;
56
54
  /** One connect attempt. Python takes the same figure from `bleak-retry-connector`. */
57
55
  export declare const DEFAULT_CONNECT_TIMEOUT_MS = 20000;
58
- /** One scan. It must outlast the backend's window, which Homey fixes at about 5 s. */
56
+ /** One scan. It must outlast the backend's window, which Homey fixes at about 10 s: its adapter sets its own bound. */
59
57
  export declare const DEFAULT_SCAN_TIMEOUT_MS = 10000;
60
58
  /** Bound on tearing down a link: the failure behind it is what gets reported. */
61
59
  export declare const LINK_TEARDOWN_TIMEOUT_MS = 5000;
@@ -16,8 +16,6 @@ export declare const NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb";
16
16
  export declare const APPEARANCE_UUID = "00002a01-0000-1000-8000-00805f9b34fb";
17
17
  /** Generic Access, for a backend that needs a service UUID to reach the two above. */
18
18
  export declare const GENERIC_ACCESS_UUID = "00001800-0000-1000-8000-00805f9b34fb";
19
- /** Client Characteristic Configuration. Only an ESPHome proxy needs it written by hand. */
20
- export declare const CCCD_UUID = "00002902-0000-1000-8000-00805f9b34fb";
21
19
  /** Largest frame accepted, length prefix included. Tesla's Go SDK allows 2 more bytes. */
22
20
  export declare const MAX_FRAME_SIZE = 1024;
23
21
  /**
@@ -55,7 +53,7 @@ export declare const DEFAULT_PAIR_POLL_INTERVAL_MS = 5000;
55
53
  export declare const DEFAULT_CONNECT_ATTEMPTS = 3;
56
54
  /** One connect attempt. Python takes the same figure from `bleak-retry-connector`. */
57
55
  export declare const DEFAULT_CONNECT_TIMEOUT_MS = 20000;
58
- /** One scan. It must outlast the backend's window, which Homey fixes at about 5 s. */
56
+ /** One scan. It must outlast the backend's window, which Homey fixes at about 10 s: its adapter sets its own bound. */
59
57
  export declare const DEFAULT_SCAN_TIMEOUT_MS = 10000;
60
58
  /** Bound on tearing down a link: the failure behind it is what gets reported. */
61
59
  export declare const LINK_TEARDOWN_TIMEOUT_MS = 5000;
@@ -17,8 +17,6 @@ const NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb";
17
17
  const APPEARANCE_UUID = "00002a01-0000-1000-8000-00805f9b34fb";
18
18
  /** Generic Access, for a backend that needs a service UUID to reach the two above. */
19
19
  const GENERIC_ACCESS_UUID = "00001800-0000-1000-8000-00805f9b34fb";
20
- /** Client Characteristic Configuration. Only an ESPHome proxy needs it written by hand. */
21
- const CCCD_UUID = "00002902-0000-1000-8000-00805f9b34fb";
22
20
  /** Largest frame accepted, length prefix included. Tesla's Go SDK allows 2 more bytes. */
23
21
  const MAX_FRAME_SIZE = 1024;
24
22
  /**
@@ -56,7 +54,7 @@ const DEFAULT_PAIR_POLL_INTERVAL_MS = 5e3;
56
54
  const DEFAULT_CONNECT_ATTEMPTS = 3;
57
55
  /** One connect attempt. Python takes the same figure from `bleak-retry-connector`. */
58
56
  const DEFAULT_CONNECT_TIMEOUT_MS = 2e4;
59
- /** One scan. It must outlast the backend's window, which Homey fixes at about 5 s. */
57
+ /** One scan. It must outlast the backend's window, which Homey fixes at about 10 s: its adapter sets its own bound. */
60
58
  const DEFAULT_SCAN_TIMEOUT_MS = 1e4;
61
59
  /** Bound on tearing down a link: the failure behind it is what gets reported. */
62
60
  const LINK_TEARDOWN_TIMEOUT_MS = 5e3;
@@ -69,4 +67,4 @@ const LINK_TEARDOWN_TIMEOUT_MS = 5e3;
69
67
  const GATT_WRITE_TIMEOUT_MS = 5e3;
70
68
  /** One read call, other than a keepalive's. */
71
69
  const GATT_READ_TIMEOUT_MS = 5e3;
72
- export { APPEARANCE_UUID, CCCD_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, WAKE_RESEND_INTERVAL_MS, WRITE_UUID };
70
+ export { APPEARANCE_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, WAKE_RESEND_INTERVAL_MS, WRITE_UUID };
@@ -5,11 +5,19 @@ const require_transport = require("./transport.cjs");
5
5
  const require_shared = require("./adapters/shared.cjs");
6
6
  // src/bluetooth/adapters/homey.ts
7
7
  /**
8
- * Bytes per write. Homey hides the MTU, so this is the ATT minimum, the one size
9
- * sure to work. BLE-00 step 6: if a whole 114-byte frame gets a reply, this can
10
- * become `null`. Otherwise step 7 keeps it, or the MTU estimate less 3 if larger.
8
+ * Bytes per write: `null`, a whole frame in one write. Homey hides the MTU.
9
+ * BLE-00 (HW-0, 2026-11-03) measured a 114-byte frame in one write at 232 ms to
10
+ * a reply, against 562 ms for 6 writes of 20 bytes. Only 114 bytes were tried:
11
+ * a signed frame (about 200 bytes) is confirmed by HW-5. Pass `maxWriteSize: 20`
12
+ * to `HomeyBleTransport` to chunk at the ATT minimum if that run shows a failure.
11
13
  */
12
- const HOMEY_MAX_WRITE_SIZE = 20;
14
+ const HOMEY_MAX_WRITE_SIZE = null;
15
+ /**
16
+ * Bound on one scan. `discover()` takes a fixed 10.04 s when it is not cached
17
+ * (HW-0), so the library's 10 s default would time out about 40 ms early.
18
+ * `VehicleBluetooth` uses this when `scan_timeout_ms` is not set.
19
+ */
20
+ const HOMEY_SCAN_TIMEOUT_MS = 15e3;
13
21
  /**
14
22
  * For `connect_timeout_ms` and `max_connect_attempts`, not applied by this
15
23
  * adapter: `discover()` caches for 30 s, so a fast retry sees old data, and
@@ -121,10 +129,11 @@ var HomeyBleTransport = class {
121
129
  ble;
122
130
  maxWriteSize;
123
131
  claims = new require_shared.LinkClaims();
132
+ scanTimeoutMs = HOMEY_SCAN_TIMEOUT_MS;
124
133
  /** @param ble `this.homey.ble`. The app needs the `homey:wireless:ble` permission. */
125
134
  constructor(ble, options = {}) {
126
135
  this.ble = ble;
127
- this.maxWriteSize = options.maxWriteSize === void 0 ? 20 : options.maxWriteSize;
136
+ this.maxWriteSize = options.maxWriteSize === void 0 ? null : options.maxWriteSize;
128
137
  }
129
138
  /** Takes Homey's fixed window, whatever `timeoutMs` says. Never pass `discover` a filter. */
130
139
  async scan({ matches }) {
@@ -149,6 +158,7 @@ var HomeyBleTransport = class {
149
158
  }
150
159
  };
151
160
  exports.HOMEY_MAX_WRITE_SIZE = HOMEY_MAX_WRITE_SIZE;
161
+ exports.HOMEY_SCAN_TIMEOUT_MS = HOMEY_SCAN_TIMEOUT_MS;
152
162
  exports.HOMEY_SETUP_TIMEOUT_MS = HOMEY_SETUP_TIMEOUT_MS;
153
163
  exports.HOMEY_UNSUBSCRIBE_TIMEOUT_MS = HOMEY_UNSUBSCRIBE_TIMEOUT_MS;
154
164
  exports.HOMEY_VEHICLE_OPTIONS = HOMEY_VEHICLE_OPTIONS;
@@ -1,10 +1,18 @@
1
1
  import { BleConnection, BleDeviceRef, BleLinkHandlers, BleScanOptions, BleTransport } from "./transport.cjs";
2
2
  /**
3
- * Bytes per write. Homey hides the MTU, so this is the ATT minimum, the one size
4
- * sure to work. BLE-00 step 6: if a whole 114-byte frame gets a reply, this can
5
- * become `null`. Otherwise step 7 keeps it, or the MTU estimate less 3 if larger.
3
+ * Bytes per write: `null`, a whole frame in one write. Homey hides the MTU.
4
+ * BLE-00 (HW-0, 2026-11-03) measured a 114-byte frame in one write at 232 ms to
5
+ * a reply, against 562 ms for 6 writes of 20 bytes. Only 114 bytes were tried:
6
+ * a signed frame (about 200 bytes) is confirmed by HW-5. Pass `maxWriteSize: 20`
7
+ * to `HomeyBleTransport` to chunk at the ATT minimum if that run shows a failure.
6
8
  */
7
- export declare const HOMEY_MAX_WRITE_SIZE = 20;
9
+ export declare const HOMEY_MAX_WRITE_SIZE: null;
10
+ /**
11
+ * Bound on one scan. `discover()` takes a fixed 10.04 s when it is not cached
12
+ * (HW-0), so the library's 10 s default would time out about 40 ms early.
13
+ * `VehicleBluetooth` uses this when `scan_timeout_ms` is not set.
14
+ */
15
+ export declare const HOMEY_SCAN_TIMEOUT_MS = 15000;
8
16
  /**
9
17
  * For `connect_timeout_ms` and `max_connect_attempts`, not applied by this
10
18
  * adapter: `discover()` caches for 30 s, so a fast retry sees old data, and
@@ -55,7 +63,7 @@ export interface HomeyAdvertisementLike {
55
63
  }
56
64
  /** What this adapter needs of `this.homey.ble`. */
57
65
  export interface HomeyBleManagerLike {
58
- /** About 5 s, fixed. Called with no filter, because the vehicle advertises no service. */
66
+ /** About 10 s, fixed, when not cached (BLE-00 HW-0). Called with no filter, because the vehicle advertises no service. */
59
67
  discover(): Promise<HomeyAdvertisementLike[]>;
60
68
  find(uuid: string): Promise<HomeyAdvertisementLike>;
61
69
  }
@@ -67,6 +75,7 @@ export declare class HomeyBleTransport implements BleTransport {
67
75
  private readonly ble;
68
76
  private readonly maxWriteSize;
69
77
  private readonly claims;
78
+ readonly scanTimeoutMs = 15000;
70
79
  /** @param ble `this.homey.ble`. The app needs the `homey:wireless:ble` permission. */
71
80
  constructor(ble: HomeyBleManagerLike, options?: HomeyBleTransportOptions);
72
81
  /** Takes Homey's fixed window, whatever `timeoutMs` says. Never pass `discover` a filter. */
@@ -1,10 +1,18 @@
1
1
  import { BleConnection, BleDeviceRef, BleLinkHandlers, BleScanOptions, BleTransport } from "./transport.mjs";
2
2
  /**
3
- * Bytes per write. Homey hides the MTU, so this is the ATT minimum, the one size
4
- * sure to work. BLE-00 step 6: if a whole 114-byte frame gets a reply, this can
5
- * become `null`. Otherwise step 7 keeps it, or the MTU estimate less 3 if larger.
3
+ * Bytes per write: `null`, a whole frame in one write. Homey hides the MTU.
4
+ * BLE-00 (HW-0, 2026-11-03) measured a 114-byte frame in one write at 232 ms to
5
+ * a reply, against 562 ms for 6 writes of 20 bytes. Only 114 bytes were tried:
6
+ * a signed frame (about 200 bytes) is confirmed by HW-5. Pass `maxWriteSize: 20`
7
+ * to `HomeyBleTransport` to chunk at the ATT minimum if that run shows a failure.
6
8
  */
7
- export declare const HOMEY_MAX_WRITE_SIZE = 20;
9
+ export declare const HOMEY_MAX_WRITE_SIZE: null;
10
+ /**
11
+ * Bound on one scan. `discover()` takes a fixed 10.04 s when it is not cached
12
+ * (HW-0), so the library's 10 s default would time out about 40 ms early.
13
+ * `VehicleBluetooth` uses this when `scan_timeout_ms` is not set.
14
+ */
15
+ export declare const HOMEY_SCAN_TIMEOUT_MS = 15000;
8
16
  /**
9
17
  * For `connect_timeout_ms` and `max_connect_attempts`, not applied by this
10
18
  * adapter: `discover()` caches for 30 s, so a fast retry sees old data, and
@@ -55,7 +63,7 @@ export interface HomeyAdvertisementLike {
55
63
  }
56
64
  /** What this adapter needs of `this.homey.ble`. */
57
65
  export interface HomeyBleManagerLike {
58
- /** About 5 s, fixed. Called with no filter, because the vehicle advertises no service. */
66
+ /** About 10 s, fixed, when not cached (BLE-00 HW-0). Called with no filter, because the vehicle advertises no service. */
59
67
  discover(): Promise<HomeyAdvertisementLike[]>;
60
68
  find(uuid: string): Promise<HomeyAdvertisementLike>;
61
69
  }
@@ -67,6 +75,7 @@ export declare class HomeyBleTransport implements BleTransport {
67
75
  private readonly ble;
68
76
  private readonly maxWriteSize;
69
77
  private readonly claims;
78
+ readonly scanTimeoutMs = 15000;
70
79
  /** @param ble `this.homey.ble`. The app needs the `homey:wireless:ble` permission. */
71
80
  constructor(ble: HomeyBleManagerLike, options?: HomeyBleTransportOptions);
72
81
  /** Takes Homey's fixed window, whatever `timeoutMs` says. Never pass `discover` a filter. */
@@ -4,11 +4,19 @@ import { BleNotSubmittedError } from "./transport.mjs";
4
4
  import { LinkClaims, compactUuid, quietly } from "./adapters/shared.mjs";
5
5
  // src/bluetooth/adapters/homey.ts
6
6
  /**
7
- * Bytes per write. Homey hides the MTU, so this is the ATT minimum, the one size
8
- * sure to work. BLE-00 step 6: if a whole 114-byte frame gets a reply, this can
9
- * become `null`. Otherwise step 7 keeps it, or the MTU estimate less 3 if larger.
7
+ * Bytes per write: `null`, a whole frame in one write. Homey hides the MTU.
8
+ * BLE-00 (HW-0, 2026-11-03) measured a 114-byte frame in one write at 232 ms to
9
+ * a reply, against 562 ms for 6 writes of 20 bytes. Only 114 bytes were tried:
10
+ * a signed frame (about 200 bytes) is confirmed by HW-5. Pass `maxWriteSize: 20`
11
+ * to `HomeyBleTransport` to chunk at the ATT minimum if that run shows a failure.
10
12
  */
11
- const HOMEY_MAX_WRITE_SIZE = 20;
13
+ const HOMEY_MAX_WRITE_SIZE = null;
14
+ /**
15
+ * Bound on one scan. `discover()` takes a fixed 10.04 s when it is not cached
16
+ * (HW-0), so the library's 10 s default would time out about 40 ms early.
17
+ * `VehicleBluetooth` uses this when `scan_timeout_ms` is not set.
18
+ */
19
+ const HOMEY_SCAN_TIMEOUT_MS = 15e3;
12
20
  /**
13
21
  * For `connect_timeout_ms` and `max_connect_attempts`, not applied by this
14
22
  * adapter: `discover()` caches for 30 s, so a fast retry sees old data, and
@@ -120,10 +128,11 @@ var HomeyBleTransport = class {
120
128
  ble;
121
129
  maxWriteSize;
122
130
  claims = new LinkClaims();
131
+ scanTimeoutMs = HOMEY_SCAN_TIMEOUT_MS;
123
132
  /** @param ble `this.homey.ble`. The app needs the `homey:wireless:ble` permission. */
124
133
  constructor(ble, options = {}) {
125
134
  this.ble = ble;
126
- this.maxWriteSize = options.maxWriteSize === void 0 ? 20 : options.maxWriteSize;
135
+ this.maxWriteSize = options.maxWriteSize === void 0 ? null : options.maxWriteSize;
127
136
  }
128
137
  /** Takes Homey's fixed window, whatever `timeoutMs` says. Never pass `discover` a filter. */
129
138
  async scan({ matches }) {
@@ -147,4 +156,4 @@ var HomeyBleTransport = class {
147
156
  return connection;
148
157
  }
149
158
  };
150
- export { HOMEY_MAX_WRITE_SIZE, HOMEY_SETUP_TIMEOUT_MS, HOMEY_UNSUBSCRIBE_TIMEOUT_MS, HOMEY_VEHICLE_OPTIONS, HomeyBleTransport };
159
+ export { HOMEY_MAX_WRITE_SIZE, HOMEY_SCAN_TIMEOUT_MS, HOMEY_SETUP_TIMEOUT_MS, HOMEY_UNSUBSCRIBE_TIMEOUT_MS, HOMEY_VEHICLE_OPTIONS, HomeyBleTransport };
@@ -80,6 +80,8 @@ export interface BleScanOptions {
80
80
  matches?: (name: string) => boolean;
81
81
  }
82
82
  export interface BleTransport {
83
+ /** How long one scan needs, for a backend with a fixed window. `scan_timeout_ms` overrides it. */
84
+ readonly scanTimeoutMs?: number;
83
85
  /** Active, unfiltered scan. Resolves once a match is found, or when the scan ends with what was seen. */
84
86
  scan(options: BleScanOptions): Promise<BleDeviceRef[]>;
85
87
  /** Connect, discover, subscribe to notify. Resolves only when notifications are live. */
@@ -80,6 +80,8 @@ export interface BleScanOptions {
80
80
  matches?: (name: string) => boolean;
81
81
  }
82
82
  export interface BleTransport {
83
+ /** How long one scan needs, for a backend with a fixed window. `scan_timeout_ms` overrides it. */
84
+ readonly scanTimeoutMs?: number;
83
85
  /** Active, unfiltered scan. Resolves once a match is found, or when the scan ends with what was seen. */
84
86
  scan(options: BleScanOptions): Promise<BleDeviceRef[]>;
85
87
  /** Connect, discover, subscribe to notify. Resolves only when notifications are live. */
@@ -142,7 +142,7 @@ var VehicleBluetooth = class extends require_broadcast.BroadcastListeners {
142
142
  this.wake_if_asleep = options.wake_if_asleep ?? true;
143
143
  this.maxConnectAttempts = options.max_connect_attempts ?? 3;
144
144
  this.connectTimeoutMs = options.connect_timeout_ms ?? 2e4;
145
- this.scanTimeoutMs = options.scan_timeout_ms ?? 1e4;
145
+ this.scanTimeoutMs = options.scan_timeout_ms ?? options.transport.scanTimeoutMs ?? 1e4;
146
146
  this.wakeProbeTimeoutMs = options.wake_probe_timeout_ms ?? 1e3;
147
147
  if (this.wakeProbeTimeoutMs <= 0) throw new RangeError("wake_probe_timeout_ms must be greater than 0.");
148
148
  this.ble_name = require_names.get_name(vin);
@@ -140,7 +140,7 @@ var VehicleBluetooth = class extends BroadcastListeners {
140
140
  this.wake_if_asleep = options.wake_if_asleep ?? true;
141
141
  this.maxConnectAttempts = options.max_connect_attempts ?? 3;
142
142
  this.connectTimeoutMs = options.connect_timeout_ms ?? 2e4;
143
- this.scanTimeoutMs = options.scan_timeout_ms ?? 1e4;
143
+ this.scanTimeoutMs = options.scan_timeout_ms ?? options.transport.scanTimeoutMs ?? 1e4;
144
144
  this.wakeProbeTimeoutMs = options.wake_probe_timeout_ms ?? 1e3;
145
145
  if (this.wakeProbeTimeoutMs <= 0) throw new RangeError("wake_probe_timeout_ms must be greater than 0.");
146
146
  this.ble_name = get_name(vin);
@@ -12,7 +12,6 @@ exports.BleBroadcastPublisher = require_streamglue.BleBroadcastPublisher;
12
12
  exports.BleBroadcastStreamGlue = require_streamglue.BleBroadcastStreamGlue;
13
13
  exports.BleNotSubmittedError = require_transport.BleNotSubmittedError;
14
14
  exports.BroadcastListeners = require_broadcast.BroadcastListeners;
15
- exports.CCCD_UUID = require_constants.CCCD_UUID;
16
15
  exports.CHARGE_PORT_VERIFY_INTERVAL_MS = require_constants.CHARGE_PORT_VERIFY_INTERVAL_MS;
17
16
  exports.CHARGE_PORT_VERIFY_TIMEOUT_MS = require_constants.CHARGE_PORT_VERIFY_TIMEOUT_MS;
18
17
  exports.DEFAULT_CONNECT_ATTEMPTS = require_constants.DEFAULT_CONNECT_ATTEMPTS;
@@ -1,5 +1,5 @@
1
1
  import { BroadcastListeners, Unsubscribe } from "./bluetooth/broadcast.cjs";
2
- import { APPEARANCE_UUID, CCCD_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, WAKE_RESEND_INTERVAL_MS, WRITE_UUID } from "./bluetooth/constants.cjs";
2
+ import { APPEARANCE_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, WAKE_RESEND_INTERVAL_MS, WRITE_UUID } from "./bluetooth/constants.cjs";
3
3
  import { ReassemblingBuffer, parseFrame, prependLength, splitFrame } from "./bluetooth/framing.cjs";
4
4
  import { get_name, name_matches_vin, valid_name } from "./bluetooth/names.cjs";
5
5
  import TeslaBluetooth, { VehiclesBluetooth } from "./bluetooth/teslabluetooth.cjs";
@@ -7,4 +7,4 @@ import { BleBroadcastPublisher, BleBroadcastStreamGlue, GEAR_STATES, StreamSink,
7
7
  import { BleCharacteristic, BleConnection, BleDeviceRef, BleLinkHandlers, BleNotSubmittedError, BleScanOptions, BleTransport } from "./bluetooth/transport.cjs";
8
8
  import { BluetoothConfirmation, VerifyPlan } from "./bluetooth/verify.cjs";
9
9
  import VehicleBluetooth, { FindVehicleOptions, PairOptions, VehicleBluetoothOptions } from "./bluetooth/vehiclebluetooth.cjs";
10
- export { APPEARANCE_UUID, BleBroadcastPublisher, BleBroadcastStreamGlue, type BleCharacteristic, type BleConnection, type BleDeviceRef, type BleLinkHandlers, BleNotSubmittedError, type BleScanOptions, type BleTransport, type BluetoothConfirmation, BroadcastListeners, CCCD_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, type FindVehicleOptions, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GEAR_STATES, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, type PairOptions, READ_UUID, ReassemblingBuffer, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, type StreamSink, TONNEAU_POSITION_STATES, TeslaBluetooth, type Unsubscribe, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, VehicleBluetooth, type VehicleBluetoothOptions, VehiclesBluetooth, type VerifyPlan, WAKE_RESEND_INTERVAL_MS, WRITE_UUID, get_name, name_matches_vin, parseFrame, prependLength, splitFrame, valid_name };
10
+ export { APPEARANCE_UUID, BleBroadcastPublisher, BleBroadcastStreamGlue, type BleCharacteristic, type BleConnection, type BleDeviceRef, type BleLinkHandlers, BleNotSubmittedError, type BleScanOptions, type BleTransport, type BluetoothConfirmation, BroadcastListeners, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, type FindVehicleOptions, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GEAR_STATES, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, type PairOptions, READ_UUID, ReassemblingBuffer, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, type StreamSink, TONNEAU_POSITION_STATES, TeslaBluetooth, type Unsubscribe, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, VehicleBluetooth, type VehicleBluetoothOptions, VehiclesBluetooth, type VerifyPlan, WAKE_RESEND_INTERVAL_MS, WRITE_UUID, get_name, name_matches_vin, parseFrame, prependLength, splitFrame, valid_name };
@@ -1,5 +1,5 @@
1
1
  import { BroadcastListeners, Unsubscribe } from "./bluetooth/broadcast.mjs";
2
- import { APPEARANCE_UUID, CCCD_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, WAKE_RESEND_INTERVAL_MS, WRITE_UUID } from "./bluetooth/constants.mjs";
2
+ import { APPEARANCE_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, WAKE_RESEND_INTERVAL_MS, WRITE_UUID } from "./bluetooth/constants.mjs";
3
3
  import { ReassemblingBuffer, parseFrame, prependLength, splitFrame } from "./bluetooth/framing.mjs";
4
4
  import { get_name, name_matches_vin, valid_name } from "./bluetooth/names.mjs";
5
5
  import TeslaBluetooth, { VehiclesBluetooth } from "./bluetooth/teslabluetooth.mjs";
@@ -7,4 +7,4 @@ import { BleBroadcastPublisher, BleBroadcastStreamGlue, GEAR_STATES, StreamSink,
7
7
  import { BleCharacteristic, BleConnection, BleDeviceRef, BleLinkHandlers, BleNotSubmittedError, BleScanOptions, BleTransport } from "./bluetooth/transport.mjs";
8
8
  import { BluetoothConfirmation, VerifyPlan } from "./bluetooth/verify.mjs";
9
9
  import VehicleBluetooth, { FindVehicleOptions, PairOptions, VehicleBluetoothOptions } from "./bluetooth/vehiclebluetooth.mjs";
10
- export { APPEARANCE_UUID, BleBroadcastPublisher, BleBroadcastStreamGlue, type BleCharacteristic, type BleConnection, type BleDeviceRef, type BleLinkHandlers, BleNotSubmittedError, type BleScanOptions, type BleTransport, type BluetoothConfirmation, BroadcastListeners, CCCD_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, type FindVehicleOptions, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GEAR_STATES, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, type PairOptions, READ_UUID, ReassemblingBuffer, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, type StreamSink, TONNEAU_POSITION_STATES, TeslaBluetooth, type Unsubscribe, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, VehicleBluetooth, type VehicleBluetoothOptions, VehiclesBluetooth, type VerifyPlan, WAKE_RESEND_INTERVAL_MS, WRITE_UUID, get_name, name_matches_vin, parseFrame, prependLength, splitFrame, valid_name };
10
+ export { APPEARANCE_UUID, BleBroadcastPublisher, BleBroadcastStreamGlue, type BleCharacteristic, type BleConnection, type BleDeviceRef, type BleLinkHandlers, BleNotSubmittedError, type BleScanOptions, type BleTransport, type BluetoothConfirmation, BroadcastListeners, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, type FindVehicleOptions, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GEAR_STATES, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, type PairOptions, READ_UUID, ReassemblingBuffer, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, type StreamSink, TONNEAU_POSITION_STATES, TeslaBluetooth, type Unsubscribe, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, VehicleBluetooth, type VehicleBluetoothOptions, VehiclesBluetooth, type VerifyPlan, WAKE_RESEND_INTERVAL_MS, WRITE_UUID, get_name, name_matches_vin, parseFrame, prependLength, splitFrame, valid_name };
@@ -1,9 +1,9 @@
1
1
  import { BroadcastListeners } from "./bluetooth/broadcast.mjs";
2
- import { APPEARANCE_UUID, CCCD_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, WAKE_RESEND_INTERVAL_MS, WRITE_UUID } from "./bluetooth/constants.mjs";
2
+ import { APPEARANCE_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, WAKE_RESEND_INTERVAL_MS, WRITE_UUID } from "./bluetooth/constants.mjs";
3
3
  import { ReassemblingBuffer, parseFrame, prependLength, splitFrame } from "./bluetooth/framing.mjs";
4
4
  import { get_name, name_matches_vin, valid_name } from "./bluetooth/names.mjs";
5
5
  import TeslaBluetooth, { VehiclesBluetooth } from "./bluetooth/teslabluetooth.mjs";
6
6
  import { BleBroadcastPublisher, BleBroadcastStreamGlue, GEAR_STATES, TONNEAU_POSITION_STATES } from "./bluetooth/streamglue.mjs";
7
7
  import { BleNotSubmittedError } from "./bluetooth/transport.mjs";
8
8
  import VehicleBluetooth from "./bluetooth/vehiclebluetooth.mjs";
9
- export { APPEARANCE_UUID, BleBroadcastPublisher, BleBroadcastStreamGlue, BleNotSubmittedError, BroadcastListeners, CCCD_UUID, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GEAR_STATES, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, ReassemblingBuffer, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, TONNEAU_POSITION_STATES, TeslaBluetooth, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, VehicleBluetooth, VehiclesBluetooth, WAKE_RESEND_INTERVAL_MS, WRITE_UUID, get_name, name_matches_vin, parseFrame, prependLength, splitFrame, valid_name };
9
+ export { APPEARANCE_UUID, BleBroadcastPublisher, BleBroadcastStreamGlue, BleNotSubmittedError, BroadcastListeners, CHARGE_PORT_VERIFY_INTERVAL_MS, CHARGE_PORT_VERIFY_TIMEOUT_MS, DEFAULT_CONNECT_ATTEMPTS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_KEEPALIVE_INTERVAL_MS, DEFAULT_MAX_WRITE_SIZE, DEFAULT_PAIR_POLL_INTERVAL_MS, DEFAULT_PAIR_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, DEFAULT_WAKE_PROBE_TIMEOUT_MS, DEFAULT_WAKE_TIMEOUT_MS, GATT_READ_TIMEOUT_MS, GATT_WRITE_TIMEOUT_MS, GEAR_STATES, GENERIC_ACCESS_UUID, INFOTAINMENT_BOOT_TIMEOUT_MS, LINK_TEARDOWN_TIMEOUT_MS, MAX_FRAME_SIZE, NAME_UUID, READ_UUID, ReassemblingBuffer, SERVICE_UUID, STALE_CHUNK_TIMEOUT_MS, TONNEAU_POSITION_STATES, TeslaBluetooth, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID, VehicleBluetooth, VehiclesBluetooth, WAKE_RESEND_INTERVAL_MS, WRITE_UUID, get_name, name_matches_vin, parseFrame, prependLength, splitFrame, valid_name };
@@ -127,7 +127,7 @@ var TeslaFleetApi = class {
127
127
  if (method === "GET") json = null;
128
128
  const headers = {
129
129
  Authorization: `Bearer ${await this.access_token()}`,
130
- "X-Library": `node tesla_fleet_api 0.5.0`
130
+ "X-Library": `node tesla_fleet_api 0.6.0`
131
131
  };
132
132
  const query = new URLSearchParams();
133
133
  if (params) {
@@ -127,7 +127,7 @@ var TeslaFleetApi = class {
127
127
  if (method === "GET") json = null;
128
128
  const headers = {
129
129
  Authorization: `Bearer ${await this.access_token()}`,
130
- "X-Library": `node tesla_fleet_api 0.5.0`
130
+ "X-Library": `node tesla_fleet_api 0.6.0`
131
131
  };
132
132
  const query = new URLSearchParams();
133
133
  if (params) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tesla-fleet-api",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -122,16 +122,6 @@
122
122
  "default": "./dist/bluetooth/noble.cjs"
123
123
  }
124
124
  },
125
- "./bluetooth/esphome": {
126
- "import": {
127
- "types": "./dist/bluetooth/esphome.d.mts",
128
- "default": "./dist/bluetooth/esphome.mjs"
129
- },
130
- "require": {
131
- "types": "./dist/bluetooth/esphome.d.cts",
132
- "default": "./dist/bluetooth/esphome.cjs"
133
- }
134
- },
135
125
  "./powerwall": {
136
126
  "import": {
137
127
  "types": "./dist/powerwall.d.mts",
@@ -153,13 +143,9 @@
153
143
  "@teslemetry/tesla-protocol": "~6.1.0"
154
144
  },
155
145
  "peerDependencies": {
156
- "@2colors/esphome-native-api": "^1.3.6",
157
146
  "@stoprocent/noble": "^2.8.0"
158
147
  },
159
148
  "peerDependenciesMeta": {
160
- "@2colors/esphome-native-api": {
161
- "optional": true
162
- },
163
149
  "@stoprocent/noble": {
164
150
  "optional": true
165
151
  }
@@ -1,292 +0,0 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_async = require("./async.cjs");
3
- const require_constants = require("./constants.cjs");
4
- const require_transport = require("./transport.cjs");
5
- const require_shared = require("./adapters/shared.cjs");
6
- // src/bluetooth/adapters/esphome.ts
7
- /** The bytes that turn notifications on in a CCCD. */
8
- const CCCD_ENABLE_NOTIFICATIONS = Buffer.from([1, 0]);
9
- /** How long a proxy takes to answer a connect. The package's own wait is the same. */
10
- const DEVICE_CONNECT_WAIT_MS = 1e4;
11
- /** How long the API connection to the proxy may take. */
12
- const API_CONNECT_TIMEOUT_MS = 1e4;
13
- const ADVERTISEMENT = "message.BluetoothLEAdvertisementResponse";
14
- const CONNECTION_STATE = "message.BluetoothDeviceConnectionResponse";
15
- const NOTIFY_DATA = "message.BluetoothGATTNotifyDataResponse";
16
- /** One queue per proxy, however many transports or links share it. */
17
- const proxyQueues = /* @__PURE__ */ new WeakMap();
18
- function queueFor(proxy) {
19
- let queue = proxyQueues.get(proxy);
20
- if (!queue) {
21
- queue = new require_async.AsyncLock();
22
- proxyQueues.set(proxy, queue);
23
- }
24
- return queue;
25
- }
26
- /** A proxy's 48-bit address as a MAC, the form a caller would recognize. */
27
- function formatAddress(address) {
28
- return address.toString(16).padStart(12, "0").replace(/(..)(?=.)/g, "$1:").toUpperCase();
29
- }
30
- function parseAddress(id) {
31
- const hex = id.replace(/:/g, "");
32
- if (!/^[0-9a-fA-F]{12}$/.test(hex)) throw new Error("An ESPHome device id must be a 48-bit address.");
33
- return Number.parseInt(hex, 16);
34
- }
35
- function sameUuid(entry, uuid) {
36
- return entry.uuid ? require_shared.compactUuid(entry.uuid) === require_shared.compactUuid(uuid) : (entry.shortUuid ?? 0).toString(16).padStart(4, "0") === require_shared.compactUuid(uuid);
37
- }
38
- /** A late reply to an abandoned call can land on the next call of its type. */
39
- function assertReplyFor(reply, address, handle) {
40
- if (reply.address !== address || reply.handle !== handle) throw new Error("The proxy's reply was for another request.");
41
- }
42
- var EsphomeConnection = class {
43
- proxy;
44
- address;
45
- id;
46
- handlers;
47
- claims;
48
- lost = false;
49
- closed = false;
50
- closing;
51
- handles = /* @__PURE__ */ new Map();
52
- mtu = 0;
53
- onLost = () => {
54
- if (this.lost) return;
55
- this.lost = true;
56
- this.stopListening();
57
- if (!this.closed) this.handlers.onDisconnect();
58
- };
59
- onState = (state) => {
60
- if (state.address === this.address && !state.connected) this.onLost();
61
- };
62
- notifyHandle;
63
- onNotifyData = (message) => {
64
- if (message.address === this.address && message.handle === this.notifyHandle) this.handlers.onNotify(Buffer.from(message.data, "base64"));
65
- };
66
- constructor(proxy, address, id, handlers, claims) {
67
- this.proxy = proxy;
68
- this.address = address;
69
- this.id = id;
70
- this.handlers = handlers;
71
- this.claims = claims;
72
- }
73
- get queue() {
74
- return queueFor(this.proxy);
75
- }
76
- get isConnected() {
77
- return !this.lost && !this.closed && this.proxy.connected && this.proxy.authorized;
78
- }
79
- get maxWriteSize() {
80
- return this.mtu > 3 ? this.mtu - 3 : 20;
81
- }
82
- /** Ask the proxy to connect. The reply is matched here, as a link dropping elsewhere sends the same message type. */
83
- requestLink(addressType) {
84
- const { proxy, address } = this;
85
- return this.queue.run(async () => {
86
- let onReply;
87
- const reply = new Promise((resolve) => {
88
- onReply = (state) => {
89
- if (state.address === address) resolve(state);
90
- };
91
- });
92
- proxy.on(CONNECTION_STATE, onReply);
93
- try {
94
- return await require_async.withTimeout(() => Promise.race([reply, proxy.connectBluetoothDeviceService(address, addressType, false).then(() => reply)]), DEVICE_CONNECT_WAIT_MS);
95
- } finally {
96
- proxy.off(CONNECTION_STATE, onReply);
97
- }
98
- });
99
- }
100
- /** Connect, map the handles, and subscribe. Resolves once the CCCD write is done. */
101
- async open(addressType) {
102
- const state = await this.requestLink(addressType);
103
- if (!state.connected || state.error) {
104
- this.lost = true;
105
- throw new Error(`The proxy could not connect: error ${state.error}.`);
106
- }
107
- this.assertCurrent();
108
- this.mtu = state.mtu;
109
- const { proxy, address } = this;
110
- proxy.on(CONNECTION_STATE, this.onState);
111
- proxy.on("disconnected", this.onLost);
112
- proxy.on("unauthorized", this.onLost);
113
- const { servicesList } = await this.queue.run(() => proxy.listBluetoothGATTServicesService(address));
114
- const service = servicesList.find((candidate) => sameUuid(candidate, require_constants.SERVICE_UUID));
115
- const characteristic = (uuid) => service?.characteristicsList.find((candidate) => sameUuid(candidate, uuid));
116
- const write = characteristic(require_constants.VEHICLE_WRITE_UUID);
117
- const notify = characteristic(require_constants.VEHICLE_NOTIFY_UUID);
118
- const cccd = notify?.descriptorsList.find((descriptor) => sameUuid(descriptor, require_constants.CCCD_UUID));
119
- if (!write || !notify || !cccd) throw new Error("The vehicle's write or notify characteristic, or its CCCD, was not found.");
120
- this.handles.set("write", write.handle);
121
- const version = characteristic(require_constants.VERSION_UUID);
122
- if (version) this.handles.set("version", version.handle);
123
- const genericAccess = servicesList.find((candidate) => sameUuid(candidate, require_constants.GENERIC_ACCESS_UUID));
124
- for (const [name, uuid] of [["deviceName", require_constants.NAME_UUID], ["appearance", require_constants.APPEARANCE_UUID]]) {
125
- const entry = genericAccess?.characteristicsList.find((candidate) => sameUuid(candidate, uuid));
126
- if (entry) this.handles.set(name, entry.handle);
127
- }
128
- this.assertCurrent();
129
- this.notifyHandle = notify.handle;
130
- proxy.on(NOTIFY_DATA, this.onNotifyData);
131
- await this.queue.run(() => proxy.notifyBluetoothGATTCharacteristicService(address, notify.handle));
132
- await this.queue.run(() => proxy.writeBluetoothGATTDescriptorService(address, cccd.handle, CCCD_ENABLE_NOTIFICATIONS));
133
- }
134
- /** A superseded attempt stops here, before it changes a link another attempt owns. */
135
- assertCurrent() {
136
- if (this.claims.superseded(this.id, this)) throw new Error("A newer connect attempt took over this link.");
137
- }
138
- async write(data) {
139
- const { proxy, address } = this;
140
- const handle = this.handles.get("write");
141
- if (handle === void 0) throw new require_transport.BleNotSubmittedError("The vehicle write characteristic was not found.");
142
- const queuedAt = performance.now();
143
- await this.queue.run(async () => {
144
- if (!this.isConnected) throw new require_transport.BleNotSubmittedError("The link is down.");
145
- if (performance.now() - queuedAt >= 5e3) throw new require_transport.BleNotSubmittedError("The write outlasted its timeout in the proxy queue.");
146
- assertReplyFor(await proxy.writeBluetoothGATTCharacteristicService(address, handle, Buffer.from(data), true), address, handle);
147
- });
148
- }
149
- async read(characteristic) {
150
- const { proxy, address } = this;
151
- const handle = this.handles.get(characteristic);
152
- if (handle === void 0) throw new require_transport.BleNotSubmittedError(`The ${characteristic} characteristic was not found.`);
153
- const reply = await this.queue.run(() => {
154
- if (!this.isConnected) throw new require_transport.BleNotSubmittedError("The link is down.");
155
- return proxy.readBluetoothGATTCharacteristicService(address, handle);
156
- });
157
- assertReplyFor(reply, address, handle);
158
- return Buffer.from(reply.data, "base64");
159
- }
160
- stopListening() {
161
- const { proxy } = this;
162
- proxy.off(CONNECTION_STATE, this.onState);
163
- proxy.off("disconnected", this.onLost);
164
- proxy.off("unauthorized", this.onLost);
165
- proxy.off(NOTIFY_DATA, this.onNotifyData);
166
- }
167
- disconnect() {
168
- this.closing ??= (async () => {
169
- this.closed = true;
170
- this.stopListening();
171
- const { claims, id, proxy, address } = this;
172
- const superseded = claims.superseded(id, this);
173
- claims.release(id, this);
174
- if (!superseded && !this.lost && proxy.authorized) await require_shared.quietly(() => this.queue.run(() => proxy.disconnectBluetoothDeviceService(address)));
175
- })();
176
- return this.closing;
177
- }
178
- };
179
- var EsphomeBleTransport = class {
180
- options;
181
- proxy;
182
- ownsProxy;
183
- scans = new require_async.AsyncLock();
184
- claims = new require_shared.LinkClaims();
185
- /** A proxy refuses a connect with no address type, and only an advertisement carries it. */
186
- addressTypes = /* @__PURE__ */ new Map();
187
- constructor(options) {
188
- this.options = options;
189
- this.ownsProxy = !("connection" in options);
190
- if ("connection" in options) this.proxy = options.connection;
191
- }
192
- /** The API connection, up and authorized. */
193
- async ready() {
194
- const proxy = this.proxy ??= await this.buildProxy();
195
- if (proxy.authorized) return proxy;
196
- if (this.ownsProxy && !proxy.connected) proxy.connect();
197
- let onAuthorized;
198
- let onError;
199
- try {
200
- await require_async.withTimeout(() => new Promise((resolve, reject) => {
201
- onAuthorized = resolve;
202
- onError = reject;
203
- proxy.on("authorized", onAuthorized);
204
- proxy.on("error", onError);
205
- }), API_CONNECT_TIMEOUT_MS);
206
- } finally {
207
- proxy.off("authorized", onAuthorized);
208
- proxy.off("error", onError);
209
- }
210
- return proxy;
211
- }
212
- async buildProxy() {
213
- if ("connection" in this.options) return this.options.connection;
214
- const specifier = "@2colors/esphome-native-api";
215
- let module;
216
- try {
217
- module = await import(specifier);
218
- } catch (cause) {
219
- throw new Error("EsphomeBleTransport needs @2colors/esphome-native-api, or a `connection` option.", { cause });
220
- }
221
- const Connection = module.Connection ?? module.default?.Connection;
222
- if (!Connection) throw new Error("@2colors/esphome-native-api has no Connection.");
223
- const { host, port, encryptionKey, password } = this.options;
224
- const proxy = new Connection({
225
- host,
226
- port,
227
- encryptionKey,
228
- password
229
- });
230
- proxy.on("error", () => void 0);
231
- return proxy;
232
- }
233
- async scan({ timeoutMs, matches }) {
234
- const proxy = await this.ready();
235
- return this.listen(proxy, timeoutMs, (device) => device.name !== void 0 && matches?.(device.name) === true);
236
- }
237
- /** Listen to advertisements until `done` accepts a device or the window ends. A proxy filters nothing. */
238
- listen(proxy, timeoutMs, done) {
239
- return this.scans.run(async () => {
240
- const seen = /* @__PURE__ */ new Map();
241
- let finish;
242
- const finished = new Promise((resolve) => {
243
- finish = resolve;
244
- });
245
- const onAdvertisement = (advertisement) => {
246
- const id = formatAddress(advertisement.address);
247
- this.addressTypes.set(id, advertisement.addressType ?? this.addressTypes.get(id));
248
- const device = {
249
- id,
250
- name: advertisement.name || seen.get(id)?.name,
251
- rssi: advertisement.rssi
252
- };
253
- seen.set(id, device);
254
- if (done(device)) finish();
255
- };
256
- proxy.on(ADVERTISEMENT, onAdvertisement);
257
- const timer = setTimeout(finish, Math.max(0, timeoutMs - 250));
258
- try {
259
- proxy.subscribeBluetoothAdvertisementService();
260
- await finished;
261
- } finally {
262
- clearTimeout(timer);
263
- proxy.off(ADVERTISEMENT, onAdvertisement);
264
- await require_shared.quietly(() => proxy.unsubscribeBluetoothAdvertisementService());
265
- }
266
- return [...seen.values()];
267
- });
268
- }
269
- async connect(device, handlers) {
270
- const address = parseAddress(device.id);
271
- const id = formatAddress(address);
272
- const proxy = await this.ready();
273
- if (this.addressTypes.get(id) === void 0) await this.listen(proxy, require_constants.DEFAULT_SCAN_TIMEOUT_MS, (seen) => seen.id === id && this.addressTypes.get(id) !== void 0);
274
- const connection = new EsphomeConnection(proxy, address, id, handlers, this.claims);
275
- this.claims.claim(id, connection);
276
- try {
277
- await connection.open(this.addressTypes.get(id));
278
- } catch (error) {
279
- await connection.disconnect();
280
- throw error;
281
- }
282
- return connection;
283
- }
284
- /** Close the API connection, if this transport opened it. */
285
- close() {
286
- if (this.ownsProxy) {
287
- this.proxy?.disconnect();
288
- this.proxy = void 0;
289
- }
290
- }
291
- };
292
- exports.EsphomeBleTransport = EsphomeBleTransport;
@@ -1,83 +0,0 @@
1
- import { BleConnection, BleDeviceRef, BleLinkHandlers, BleScanOptions, BleTransport } from "./transport.cjs";
2
- export interface EsphomeAdvertisement {
3
- address: number;
4
- addressType?: number;
5
- name?: string;
6
- rssi?: number;
7
- }
8
- export interface EsphomeConnectionState {
9
- address: number;
10
- connected: boolean;
11
- mtu: number;
12
- error: number;
13
- }
14
- export interface EsphomeGattDescriptor {
15
- uuid?: string;
16
- /** Set in place of `uuid` for a short UUID, on API 1.12 and up. */
17
- shortUuid?: number;
18
- handle: number;
19
- }
20
- export interface EsphomeGattCharacteristic extends EsphomeGattDescriptor {
21
- descriptorsList: EsphomeGattDescriptor[];
22
- }
23
- export interface EsphomeGattService extends EsphomeGattDescriptor {
24
- characteristicsList: EsphomeGattCharacteristic[];
25
- }
26
- export interface EsphomeGattReply {
27
- address: number;
28
- handle: number;
29
- }
30
- export interface EsphomeGattData extends EsphomeGattReply {
31
- data: string;
32
- }
33
- /** The parts of the package's `Connection` this adapter uses. */
34
- export interface EsphomeConnectionLike {
35
- connected: boolean;
36
- authorized: boolean;
37
- connect(): void;
38
- disconnect(): void;
39
- on(event: string, listener: (...args: any[]) => void): unknown;
40
- off(event: string, listener: (...args: any[]) => void): unknown;
41
- subscribeBluetoothAdvertisementService(): void;
42
- unsubscribeBluetoothAdvertisementService(): void;
43
- connectBluetoothDeviceService(address: number, addressType?: number, useCache?: boolean): Promise<unknown>;
44
- disconnectBluetoothDeviceService(address: number): Promise<unknown>;
45
- listBluetoothGATTServicesService(address: number): Promise<{
46
- servicesList: EsphomeGattService[];
47
- }>;
48
- readBluetoothGATTCharacteristicService(address: number, handle: number): Promise<EsphomeGattData>;
49
- writeBluetoothGATTCharacteristicService(address: number, handle: number, value: Uint8Array, response?: boolean): Promise<EsphomeGattReply>;
50
- notifyBluetoothGATTCharacteristicService(address: number, handle: number): Promise<unknown>;
51
- writeBluetoothGATTDescriptorService(address: number, handle: number, value: Uint8Array): Promise<void>;
52
- }
53
- export type EsphomeBleTransportOptions = {
54
- /** A `Connection` you own. This adapter never closes it. */
55
- connection: EsphomeConnectionLike;
56
- } | {
57
- /** The proxy's address. `@2colors/esphome-native-api` loads on first use. */
58
- host: string;
59
- /** Defaults to the package's 6053. */
60
- port?: number;
61
- /** The proxy's Noise key, base64. Never logged. */
62
- encryptionKey?: string;
63
- password?: string;
64
- };
65
- export declare class EsphomeBleTransport implements BleTransport {
66
- private readonly options;
67
- private proxy;
68
- private readonly ownsProxy;
69
- private readonly scans;
70
- private readonly claims;
71
- /** A proxy refuses a connect with no address type, and only an advertisement carries it. */
72
- private readonly addressTypes;
73
- constructor(options: EsphomeBleTransportOptions);
74
- /** The API connection, up and authorized. */
75
- private ready;
76
- private buildProxy;
77
- scan({ timeoutMs, matches }: BleScanOptions): Promise<BleDeviceRef[]>;
78
- /** Listen to advertisements until `done` accepts a device or the window ends. A proxy filters nothing. */
79
- private listen;
80
- connect(device: BleDeviceRef, handlers: BleLinkHandlers): Promise<BleConnection>;
81
- /** Close the API connection, if this transport opened it. */
82
- close(): void;
83
- }
@@ -1,83 +0,0 @@
1
- import { BleConnection, BleDeviceRef, BleLinkHandlers, BleScanOptions, BleTransport } from "./transport.mjs";
2
- export interface EsphomeAdvertisement {
3
- address: number;
4
- addressType?: number;
5
- name?: string;
6
- rssi?: number;
7
- }
8
- export interface EsphomeConnectionState {
9
- address: number;
10
- connected: boolean;
11
- mtu: number;
12
- error: number;
13
- }
14
- export interface EsphomeGattDescriptor {
15
- uuid?: string;
16
- /** Set in place of `uuid` for a short UUID, on API 1.12 and up. */
17
- shortUuid?: number;
18
- handle: number;
19
- }
20
- export interface EsphomeGattCharacteristic extends EsphomeGattDescriptor {
21
- descriptorsList: EsphomeGattDescriptor[];
22
- }
23
- export interface EsphomeGattService extends EsphomeGattDescriptor {
24
- characteristicsList: EsphomeGattCharacteristic[];
25
- }
26
- export interface EsphomeGattReply {
27
- address: number;
28
- handle: number;
29
- }
30
- export interface EsphomeGattData extends EsphomeGattReply {
31
- data: string;
32
- }
33
- /** The parts of the package's `Connection` this adapter uses. */
34
- export interface EsphomeConnectionLike {
35
- connected: boolean;
36
- authorized: boolean;
37
- connect(): void;
38
- disconnect(): void;
39
- on(event: string, listener: (...args: any[]) => void): unknown;
40
- off(event: string, listener: (...args: any[]) => void): unknown;
41
- subscribeBluetoothAdvertisementService(): void;
42
- unsubscribeBluetoothAdvertisementService(): void;
43
- connectBluetoothDeviceService(address: number, addressType?: number, useCache?: boolean): Promise<unknown>;
44
- disconnectBluetoothDeviceService(address: number): Promise<unknown>;
45
- listBluetoothGATTServicesService(address: number): Promise<{
46
- servicesList: EsphomeGattService[];
47
- }>;
48
- readBluetoothGATTCharacteristicService(address: number, handle: number): Promise<EsphomeGattData>;
49
- writeBluetoothGATTCharacteristicService(address: number, handle: number, value: Uint8Array, response?: boolean): Promise<EsphomeGattReply>;
50
- notifyBluetoothGATTCharacteristicService(address: number, handle: number): Promise<unknown>;
51
- writeBluetoothGATTDescriptorService(address: number, handle: number, value: Uint8Array): Promise<void>;
52
- }
53
- export type EsphomeBleTransportOptions = {
54
- /** A `Connection` you own. This adapter never closes it. */
55
- connection: EsphomeConnectionLike;
56
- } | {
57
- /** The proxy's address. `@2colors/esphome-native-api` loads on first use. */
58
- host: string;
59
- /** Defaults to the package's 6053. */
60
- port?: number;
61
- /** The proxy's Noise key, base64. Never logged. */
62
- encryptionKey?: string;
63
- password?: string;
64
- };
65
- export declare class EsphomeBleTransport implements BleTransport {
66
- private readonly options;
67
- private proxy;
68
- private readonly ownsProxy;
69
- private readonly scans;
70
- private readonly claims;
71
- /** A proxy refuses a connect with no address type, and only an advertisement carries it. */
72
- private readonly addressTypes;
73
- constructor(options: EsphomeBleTransportOptions);
74
- /** The API connection, up and authorized. */
75
- private ready;
76
- private buildProxy;
77
- scan({ timeoutMs, matches }: BleScanOptions): Promise<BleDeviceRef[]>;
78
- /** Listen to advertisements until `done` accepts a device or the window ends. A proxy filters nothing. */
79
- private listen;
80
- connect(device: BleDeviceRef, handlers: BleLinkHandlers): Promise<BleConnection>;
81
- /** Close the API connection, if this transport opened it. */
82
- close(): void;
83
- }
@@ -1,291 +0,0 @@
1
- import { AsyncLock, withTimeout } from "./async.mjs";
2
- import { APPEARANCE_UUID, CCCD_UUID, DEFAULT_SCAN_TIMEOUT_MS, GENERIC_ACCESS_UUID, NAME_UUID, SERVICE_UUID, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID } from "./constants.mjs";
3
- import { BleNotSubmittedError } from "./transport.mjs";
4
- import { LinkClaims, compactUuid, quietly } from "./adapters/shared.mjs";
5
- // src/bluetooth/adapters/esphome.ts
6
- /** The bytes that turn notifications on in a CCCD. */
7
- const CCCD_ENABLE_NOTIFICATIONS = Buffer.from([1, 0]);
8
- /** How long a proxy takes to answer a connect. The package's own wait is the same. */
9
- const DEVICE_CONNECT_WAIT_MS = 1e4;
10
- /** How long the API connection to the proxy may take. */
11
- const API_CONNECT_TIMEOUT_MS = 1e4;
12
- const ADVERTISEMENT = "message.BluetoothLEAdvertisementResponse";
13
- const CONNECTION_STATE = "message.BluetoothDeviceConnectionResponse";
14
- const NOTIFY_DATA = "message.BluetoothGATTNotifyDataResponse";
15
- /** One queue per proxy, however many transports or links share it. */
16
- const proxyQueues = /* @__PURE__ */ new WeakMap();
17
- function queueFor(proxy) {
18
- let queue = proxyQueues.get(proxy);
19
- if (!queue) {
20
- queue = new AsyncLock();
21
- proxyQueues.set(proxy, queue);
22
- }
23
- return queue;
24
- }
25
- /** A proxy's 48-bit address as a MAC, the form a caller would recognize. */
26
- function formatAddress(address) {
27
- return address.toString(16).padStart(12, "0").replace(/(..)(?=.)/g, "$1:").toUpperCase();
28
- }
29
- function parseAddress(id) {
30
- const hex = id.replace(/:/g, "");
31
- if (!/^[0-9a-fA-F]{12}$/.test(hex)) throw new Error("An ESPHome device id must be a 48-bit address.");
32
- return Number.parseInt(hex, 16);
33
- }
34
- function sameUuid(entry, uuid) {
35
- return entry.uuid ? compactUuid(entry.uuid) === compactUuid(uuid) : (entry.shortUuid ?? 0).toString(16).padStart(4, "0") === compactUuid(uuid);
36
- }
37
- /** A late reply to an abandoned call can land on the next call of its type. */
38
- function assertReplyFor(reply, address, handle) {
39
- if (reply.address !== address || reply.handle !== handle) throw new Error("The proxy's reply was for another request.");
40
- }
41
- var EsphomeConnection = class {
42
- proxy;
43
- address;
44
- id;
45
- handlers;
46
- claims;
47
- lost = false;
48
- closed = false;
49
- closing;
50
- handles = /* @__PURE__ */ new Map();
51
- mtu = 0;
52
- onLost = () => {
53
- if (this.lost) return;
54
- this.lost = true;
55
- this.stopListening();
56
- if (!this.closed) this.handlers.onDisconnect();
57
- };
58
- onState = (state) => {
59
- if (state.address === this.address && !state.connected) this.onLost();
60
- };
61
- notifyHandle;
62
- onNotifyData = (message) => {
63
- if (message.address === this.address && message.handle === this.notifyHandle) this.handlers.onNotify(Buffer.from(message.data, "base64"));
64
- };
65
- constructor(proxy, address, id, handlers, claims) {
66
- this.proxy = proxy;
67
- this.address = address;
68
- this.id = id;
69
- this.handlers = handlers;
70
- this.claims = claims;
71
- }
72
- get queue() {
73
- return queueFor(this.proxy);
74
- }
75
- get isConnected() {
76
- return !this.lost && !this.closed && this.proxy.connected && this.proxy.authorized;
77
- }
78
- get maxWriteSize() {
79
- return this.mtu > 3 ? this.mtu - 3 : 20;
80
- }
81
- /** Ask the proxy to connect. The reply is matched here, as a link dropping elsewhere sends the same message type. */
82
- requestLink(addressType) {
83
- const { proxy, address } = this;
84
- return this.queue.run(async () => {
85
- let onReply;
86
- const reply = new Promise((resolve) => {
87
- onReply = (state) => {
88
- if (state.address === address) resolve(state);
89
- };
90
- });
91
- proxy.on(CONNECTION_STATE, onReply);
92
- try {
93
- return await withTimeout(() => Promise.race([reply, proxy.connectBluetoothDeviceService(address, addressType, false).then(() => reply)]), DEVICE_CONNECT_WAIT_MS);
94
- } finally {
95
- proxy.off(CONNECTION_STATE, onReply);
96
- }
97
- });
98
- }
99
- /** Connect, map the handles, and subscribe. Resolves once the CCCD write is done. */
100
- async open(addressType) {
101
- const state = await this.requestLink(addressType);
102
- if (!state.connected || state.error) {
103
- this.lost = true;
104
- throw new Error(`The proxy could not connect: error ${state.error}.`);
105
- }
106
- this.assertCurrent();
107
- this.mtu = state.mtu;
108
- const { proxy, address } = this;
109
- proxy.on(CONNECTION_STATE, this.onState);
110
- proxy.on("disconnected", this.onLost);
111
- proxy.on("unauthorized", this.onLost);
112
- const { servicesList } = await this.queue.run(() => proxy.listBluetoothGATTServicesService(address));
113
- const service = servicesList.find((candidate) => sameUuid(candidate, SERVICE_UUID));
114
- const characteristic = (uuid) => service?.characteristicsList.find((candidate) => sameUuid(candidate, uuid));
115
- const write = characteristic(VEHICLE_WRITE_UUID);
116
- const notify = characteristic(VEHICLE_NOTIFY_UUID);
117
- const cccd = notify?.descriptorsList.find((descriptor) => sameUuid(descriptor, CCCD_UUID));
118
- if (!write || !notify || !cccd) throw new Error("The vehicle's write or notify characteristic, or its CCCD, was not found.");
119
- this.handles.set("write", write.handle);
120
- const version = characteristic(VERSION_UUID);
121
- if (version) this.handles.set("version", version.handle);
122
- const genericAccess = servicesList.find((candidate) => sameUuid(candidate, GENERIC_ACCESS_UUID));
123
- for (const [name, uuid] of [["deviceName", NAME_UUID], ["appearance", APPEARANCE_UUID]]) {
124
- const entry = genericAccess?.characteristicsList.find((candidate) => sameUuid(candidate, uuid));
125
- if (entry) this.handles.set(name, entry.handle);
126
- }
127
- this.assertCurrent();
128
- this.notifyHandle = notify.handle;
129
- proxy.on(NOTIFY_DATA, this.onNotifyData);
130
- await this.queue.run(() => proxy.notifyBluetoothGATTCharacteristicService(address, notify.handle));
131
- await this.queue.run(() => proxy.writeBluetoothGATTDescriptorService(address, cccd.handle, CCCD_ENABLE_NOTIFICATIONS));
132
- }
133
- /** A superseded attempt stops here, before it changes a link another attempt owns. */
134
- assertCurrent() {
135
- if (this.claims.superseded(this.id, this)) throw new Error("A newer connect attempt took over this link.");
136
- }
137
- async write(data) {
138
- const { proxy, address } = this;
139
- const handle = this.handles.get("write");
140
- if (handle === void 0) throw new BleNotSubmittedError("The vehicle write characteristic was not found.");
141
- const queuedAt = performance.now();
142
- await this.queue.run(async () => {
143
- if (!this.isConnected) throw new BleNotSubmittedError("The link is down.");
144
- if (performance.now() - queuedAt >= 5e3) throw new BleNotSubmittedError("The write outlasted its timeout in the proxy queue.");
145
- assertReplyFor(await proxy.writeBluetoothGATTCharacteristicService(address, handle, Buffer.from(data), true), address, handle);
146
- });
147
- }
148
- async read(characteristic) {
149
- const { proxy, address } = this;
150
- const handle = this.handles.get(characteristic);
151
- if (handle === void 0) throw new BleNotSubmittedError(`The ${characteristic} characteristic was not found.`);
152
- const reply = await this.queue.run(() => {
153
- if (!this.isConnected) throw new BleNotSubmittedError("The link is down.");
154
- return proxy.readBluetoothGATTCharacteristicService(address, handle);
155
- });
156
- assertReplyFor(reply, address, handle);
157
- return Buffer.from(reply.data, "base64");
158
- }
159
- stopListening() {
160
- const { proxy } = this;
161
- proxy.off(CONNECTION_STATE, this.onState);
162
- proxy.off("disconnected", this.onLost);
163
- proxy.off("unauthorized", this.onLost);
164
- proxy.off(NOTIFY_DATA, this.onNotifyData);
165
- }
166
- disconnect() {
167
- this.closing ??= (async () => {
168
- this.closed = true;
169
- this.stopListening();
170
- const { claims, id, proxy, address } = this;
171
- const superseded = claims.superseded(id, this);
172
- claims.release(id, this);
173
- if (!superseded && !this.lost && proxy.authorized) await quietly(() => this.queue.run(() => proxy.disconnectBluetoothDeviceService(address)));
174
- })();
175
- return this.closing;
176
- }
177
- };
178
- var EsphomeBleTransport = class {
179
- options;
180
- proxy;
181
- ownsProxy;
182
- scans = new AsyncLock();
183
- claims = new LinkClaims();
184
- /** A proxy refuses a connect with no address type, and only an advertisement carries it. */
185
- addressTypes = /* @__PURE__ */ new Map();
186
- constructor(options) {
187
- this.options = options;
188
- this.ownsProxy = !("connection" in options);
189
- if ("connection" in options) this.proxy = options.connection;
190
- }
191
- /** The API connection, up and authorized. */
192
- async ready() {
193
- const proxy = this.proxy ??= await this.buildProxy();
194
- if (proxy.authorized) return proxy;
195
- if (this.ownsProxy && !proxy.connected) proxy.connect();
196
- let onAuthorized;
197
- let onError;
198
- try {
199
- await withTimeout(() => new Promise((resolve, reject) => {
200
- onAuthorized = resolve;
201
- onError = reject;
202
- proxy.on("authorized", onAuthorized);
203
- proxy.on("error", onError);
204
- }), API_CONNECT_TIMEOUT_MS);
205
- } finally {
206
- proxy.off("authorized", onAuthorized);
207
- proxy.off("error", onError);
208
- }
209
- return proxy;
210
- }
211
- async buildProxy() {
212
- if ("connection" in this.options) return this.options.connection;
213
- const specifier = "@2colors/esphome-native-api";
214
- let module;
215
- try {
216
- module = await import(specifier);
217
- } catch (cause) {
218
- throw new Error("EsphomeBleTransport needs @2colors/esphome-native-api, or a `connection` option.", { cause });
219
- }
220
- const Connection = module.Connection ?? module.default?.Connection;
221
- if (!Connection) throw new Error("@2colors/esphome-native-api has no Connection.");
222
- const { host, port, encryptionKey, password } = this.options;
223
- const proxy = new Connection({
224
- host,
225
- port,
226
- encryptionKey,
227
- password
228
- });
229
- proxy.on("error", () => void 0);
230
- return proxy;
231
- }
232
- async scan({ timeoutMs, matches }) {
233
- const proxy = await this.ready();
234
- return this.listen(proxy, timeoutMs, (device) => device.name !== void 0 && matches?.(device.name) === true);
235
- }
236
- /** Listen to advertisements until `done` accepts a device or the window ends. A proxy filters nothing. */
237
- listen(proxy, timeoutMs, done) {
238
- return this.scans.run(async () => {
239
- const seen = /* @__PURE__ */ new Map();
240
- let finish;
241
- const finished = new Promise((resolve) => {
242
- finish = resolve;
243
- });
244
- const onAdvertisement = (advertisement) => {
245
- const id = formatAddress(advertisement.address);
246
- this.addressTypes.set(id, advertisement.addressType ?? this.addressTypes.get(id));
247
- const device = {
248
- id,
249
- name: advertisement.name || seen.get(id)?.name,
250
- rssi: advertisement.rssi
251
- };
252
- seen.set(id, device);
253
- if (done(device)) finish();
254
- };
255
- proxy.on(ADVERTISEMENT, onAdvertisement);
256
- const timer = setTimeout(finish, Math.max(0, timeoutMs - 250));
257
- try {
258
- proxy.subscribeBluetoothAdvertisementService();
259
- await finished;
260
- } finally {
261
- clearTimeout(timer);
262
- proxy.off(ADVERTISEMENT, onAdvertisement);
263
- await quietly(() => proxy.unsubscribeBluetoothAdvertisementService());
264
- }
265
- return [...seen.values()];
266
- });
267
- }
268
- async connect(device, handlers) {
269
- const address = parseAddress(device.id);
270
- const id = formatAddress(address);
271
- const proxy = await this.ready();
272
- if (this.addressTypes.get(id) === void 0) await this.listen(proxy, DEFAULT_SCAN_TIMEOUT_MS, (seen) => seen.id === id && this.addressTypes.get(id) !== void 0);
273
- const connection = new EsphomeConnection(proxy, address, id, handlers, this.claims);
274
- this.claims.claim(id, connection);
275
- try {
276
- await connection.open(this.addressTypes.get(id));
277
- } catch (error) {
278
- await connection.disconnect();
279
- throw error;
280
- }
281
- return connection;
282
- }
283
- /** Close the API connection, if this transport opened it. */
284
- close() {
285
- if (this.ownsProxy) {
286
- this.proxy?.disconnect();
287
- this.proxy = void 0;
288
- }
289
- }
290
- };
291
- export { EsphomeBleTransport };