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 +1 -16
- package/dist/bluetooth/constants.cjs +1 -4
- package/dist/bluetooth/constants.d.cts +1 -3
- package/dist/bluetooth/constants.d.mts +1 -3
- package/dist/bluetooth/constants.mjs +2 -4
- package/dist/bluetooth/homey.cjs +15 -5
- package/dist/bluetooth/homey.d.cts +14 -5
- package/dist/bluetooth/homey.d.mts +14 -5
- package/dist/bluetooth/homey.mjs +15 -6
- package/dist/bluetooth/transport.d.cts +2 -0
- package/dist/bluetooth/transport.d.mts +2 -0
- package/dist/bluetooth/vehiclebluetooth.cjs +1 -1
- package/dist/bluetooth/vehiclebluetooth.mjs +1 -1
- package/dist/bluetooth.cjs +0 -1
- package/dist/bluetooth.d.cts +2 -2
- package/dist/bluetooth.d.mts +2 -2
- package/dist/bluetooth.mjs +2 -2
- package/dist/teslafleetapi.cjs +1 -1
- package/dist/teslafleetapi.mjs +1 -1
- package/package.json +1 -15
- package/dist/bluetooth/esphome.cjs +0 -292
- package/dist/bluetooth/esphome.d.cts +0 -83
- package/dist/bluetooth/esphome.d.mts +0 -83
- package/dist/bluetooth/esphome.mjs +0 -291
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 };
|
package/dist/bluetooth/homey.cjs
CHANGED
|
@@ -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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 =
|
|
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 ?
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
|
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. */
|
package/dist/bluetooth/homey.mjs
CHANGED
|
@@ -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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 =
|
|
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 ?
|
|
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);
|
package/dist/bluetooth.cjs
CHANGED
|
@@ -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;
|
package/dist/bluetooth.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BroadcastListeners, Unsubscribe } from "./bluetooth/broadcast.cjs";
|
|
2
|
-
import { APPEARANCE_UUID,
|
|
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,
|
|
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 };
|
package/dist/bluetooth.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BroadcastListeners, Unsubscribe } from "./bluetooth/broadcast.mjs";
|
|
2
|
-
import { APPEARANCE_UUID,
|
|
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,
|
|
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 };
|
package/dist/bluetooth.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BroadcastListeners } from "./bluetooth/broadcast.mjs";
|
|
2
|
-
import { APPEARANCE_UUID,
|
|
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,
|
|
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 };
|
package/dist/teslafleetapi.cjs
CHANGED
|
@@ -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.
|
|
130
|
+
"X-Library": `node tesla_fleet_api 0.6.0`
|
|
131
131
|
};
|
|
132
132
|
const query = new URLSearchParams();
|
|
133
133
|
if (params) {
|
package/dist/teslafleetapi.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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 };
|