tesla-fleet-api 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +284 -1
- package/dist/_virtual/_rolldown/runtime.cjs +10 -0
- package/dist/bluetooth/adapters/shared.cjs +38 -0
- package/dist/bluetooth/adapters/shared.mjs +36 -0
- package/dist/bluetooth/async.cjs +121 -0
- package/dist/bluetooth/async.d.cts +10 -0
- package/dist/bluetooth/async.d.mts +10 -0
- package/dist/bluetooth/async.mjs +116 -0
- package/dist/bluetooth/broadcast.cjs +153 -0
- package/dist/bluetooth/broadcast.d.cts +59 -0
- package/dist/bluetooth/broadcast.d.mts +59 -0
- package/dist/bluetooth/broadcast.mjs +151 -0
- package/dist/bluetooth/constants.cjs +99 -0
- package/dist/bluetooth/constants.d.cts +70 -0
- package/dist/bluetooth/constants.d.mts +70 -0
- package/dist/bluetooth/constants.mjs +72 -0
- package/dist/bluetooth/esphome.cjs +292 -0
- package/dist/bluetooth/esphome.d.cts +83 -0
- package/dist/bluetooth/esphome.d.mts +83 -0
- package/dist/bluetooth/esphome.mjs +291 -0
- package/dist/bluetooth/framing.cjs +125 -0
- package/dist/bluetooth/framing.d.cts +45 -0
- package/dist/bluetooth/framing.d.mts +45 -0
- package/dist/bluetooth/framing.mjs +122 -0
- package/dist/bluetooth/homey.cjs +155 -0
- package/dist/bluetooth/homey.d.cts +75 -0
- package/dist/bluetooth/homey.d.mts +75 -0
- package/dist/bluetooth/homey.mjs +150 -0
- package/dist/bluetooth/names.cjs +24 -0
- package/dist/bluetooth/names.d.cts +10 -0
- package/dist/bluetooth/names.d.mts +10 -0
- package/dist/bluetooth/names.mjs +20 -0
- package/dist/bluetooth/noble.cjs +178 -0
- package/dist/bluetooth/noble.d.cts +58 -0
- package/dist/bluetooth/noble.d.mts +58 -0
- package/dist/bluetooth/noble.mjs +177 -0
- package/dist/bluetooth/streamglue.cjs +157 -0
- package/dist/bluetooth/streamglue.d.cts +37 -0
- package/dist/bluetooth/streamglue.d.mts +37 -0
- package/dist/bluetooth/streamglue.mjs +154 -0
- package/dist/bluetooth/teslabluetooth.cjs +67 -0
- package/dist/bluetooth/teslabluetooth.d.cts +41 -0
- package/dist/bluetooth/teslabluetooth.d.mts +41 -0
- package/dist/bluetooth/teslabluetooth.mjs +66 -0
- package/dist/bluetooth/transport.cjs +10 -0
- package/dist/bluetooth/transport.d.cts +95 -0
- package/dist/bluetooth/transport.d.mts +95 -0
- package/dist/bluetooth/transport.mjs +10 -0
- package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
- package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
- package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
- package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
- package/dist/bluetooth/verify.cjs +93 -0
- package/dist/bluetooth/verify.d.cts +35 -0
- package/dist/bluetooth/verify.d.mts +35 -0
- package/dist/bluetooth/verify.mjs +91 -0
- package/dist/bluetooth.cjs +53 -0
- package/dist/bluetooth.d.cts +10 -0
- package/dist/bluetooth.d.mts +10 -0
- package/dist/bluetooth.mjs +9 -0
- package/dist/commands.cjs +1080 -85
- package/dist/commands.d.cts +585 -54
- package/dist/commands.d.mts +583 -51
- package/dist/commands.mjs +1084 -93
- package/dist/const.cjs +22 -0
- package/dist/const.d.cts +21 -0
- package/dist/const.d.mts +21 -0
- package/dist/const.mjs +20 -1
- package/dist/energy.cjs +24 -19
- package/dist/energy.d.cts +21 -15
- package/dist/energy.d.mts +21 -15
- package/dist/energy.mjs +24 -19
- package/dist/energyisland.cjs +38 -0
- package/dist/energyisland.mjs +37 -0
- package/dist/energyparsers.cjs +118 -0
- package/dist/energyparsers.d.cts +39 -0
- package/dist/energyparsers.d.mts +39 -0
- package/dist/energyparsers.mjs +116 -0
- package/dist/energyspecific.cjs +375 -17
- package/dist/energyspecific.d.cts +199 -19
- package/dist/energyspecific.d.mts +197 -16
- package/dist/energyspecific.mjs +370 -18
- package/dist/exceptions.cjs +3 -2
- package/dist/exceptions.d.cts +3 -2
- package/dist/exceptions.d.mts +3 -2
- package/dist/exceptions.mjs +3 -2
- package/dist/funnel.cjs +297 -0
- package/dist/funnel.d.cts +118 -0
- package/dist/funnel.d.mts +118 -0
- package/dist/funnel.mjs +292 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +22 -8
- package/dist/index.d.mts +22 -8
- package/dist/index.mjs +19 -6
- package/dist/log.cjs +75 -0
- package/dist/log.d.cts +4 -0
- package/dist/log.d.mts +4 -0
- package/dist/log.mjs +71 -0
- package/dist/oauth.cjs +143 -0
- package/dist/oauth.d.cts +69 -0
- package/dist/oauth.d.mts +69 -0
- package/dist/oauth.mjs +143 -0
- package/dist/powerwall/client.cjs +818 -0
- package/dist/powerwall/client.d.cts +297 -0
- package/dist/powerwall/client.d.mts +297 -0
- package/dist/powerwall/client.mjs +807 -0
- package/dist/powerwall/components.cjs +188 -0
- package/dist/powerwall/components.d.cts +45 -0
- package/dist/powerwall/components.d.mts +45 -0
- package/dist/powerwall/components.mjs +182 -0
- package/dist/powerwall/energysite.cjs +216 -0
- package/dist/powerwall/energysite.d.cts +104 -0
- package/dist/powerwall/energysite.d.mts +104 -0
- package/dist/powerwall/energysite.mjs +214 -0
- package/dist/powerwall/exceptions.cjs +59 -0
- package/dist/powerwall/exceptions.d.cts +38 -0
- package/dist/powerwall/exceptions.d.mts +38 -0
- package/dist/powerwall/exceptions.mjs +51 -0
- package/dist/powerwall/gateway.cjs +102 -0
- package/dist/powerwall/gateway.d.cts +57 -0
- package/dist/powerwall/gateway.d.mts +57 -0
- package/dist/powerwall/gateway.mjs +101 -0
- package/dist/powerwall/merge.cjs +163 -0
- package/dist/powerwall/merge.d.cts +63 -0
- package/dist/powerwall/merge.d.mts +63 -0
- package/dist/powerwall/merge.mjs +159 -0
- package/dist/powerwall/models.d.cts +95 -0
- package/dist/powerwall/models.d.mts +95 -0
- package/dist/powerwall/queries.cjs +17 -0
- package/dist/powerwall/queries.mjs +10 -0
- package/dist/powerwall/transport.cjs +232 -0
- package/dist/powerwall/transport.mjs +227 -0
- package/dist/powerwall.cjs +47 -0
- package/dist/powerwall.d.cts +9 -0
- package/dist/powerwall.d.mts +9 -0
- package/dist/powerwall.mjs +8 -0
- package/dist/router/base.cjs +203 -0
- package/dist/router/base.d.cts +112 -0
- package/dist/router/base.d.mts +112 -0
- package/dist/router/base.mjs +200 -0
- package/dist/router/energysite.cjs +65 -0
- package/dist/router/energysite.d.cts +47 -0
- package/dist/router/energysite.d.mts +47 -0
- package/dist/router/energysite.mjs +60 -0
- package/dist/router.cjs +13 -0
- package/dist/router.d.cts +3 -1
- package/dist/router.d.mts +3 -1
- package/dist/router.mjs +3 -1
- package/dist/signing/crypto.cjs +28 -0
- package/dist/signing/crypto.mjs +26 -1
- package/dist/signing/energy.cjs +123 -0
- package/dist/signing/energy.d.cts +45 -0
- package/dist/signing/energy.d.mts +45 -0
- package/dist/signing/energy.mjs +120 -0
- package/dist/signing/jws.cjs +229 -0
- package/dist/signing/jws.d.cts +45 -0
- package/dist/signing/jws.d.mts +45 -0
- package/dist/signing/jws.mjs +211 -0
- package/dist/signing/keys.cjs +191 -0
- package/dist/signing/keys.d.cts +21 -0
- package/dist/signing/keys.d.mts +21 -0
- package/dist/signing/keys.mjs +182 -0
- package/dist/signing/session.cjs +104 -30
- package/dist/signing/session.d.cts +60 -23
- package/dist/signing/session.d.mts +60 -23
- package/dist/signing/session.mjs +105 -31
- package/dist/tariff.cjs +27 -0
- package/dist/tariff.d.cts +11 -0
- package/dist/tariff.d.mts +11 -0
- package/dist/tariff.mjs +27 -1
- package/dist/teslafleetapi.cjs +256 -55
- package/dist/teslafleetapi.d.cts +110 -14
- package/dist/teslafleetapi.d.mts +110 -14
- package/dist/teslafleetapi.mjs +258 -57
- package/dist/teslemetry.cjs +152 -5
- package/dist/teslemetry.d.cts +95 -5
- package/dist/teslemetry.d.mts +93 -3
- package/dist/teslemetry.mjs +151 -6
- package/dist/teslemetryenergy.cjs +287 -0
- package/dist/teslemetryenergy.d.cts +154 -0
- package/dist/teslemetryenergy.d.mts +154 -0
- package/dist/teslemetryenergy.mjs +282 -0
- package/dist/teslemetryvehicle.cjs +407 -0
- package/dist/teslemetryvehicle.d.cts +238 -0
- package/dist/teslemetryvehicle.d.mts +238 -0
- package/dist/teslemetryvehicle.mjs +405 -0
- package/dist/tessie.cjs +42 -5
- package/dist/tessie.d.cts +51 -5
- package/dist/tessie.d.mts +49 -3
- package/dist/tessie.mjs +42 -5
- package/dist/tessievehicle.cjs +514 -0
- package/dist/tessievehicle.d.cts +333 -0
- package/dist/tessievehicle.d.mts +333 -0
- package/dist/tessievehicle.mjs +513 -0
- package/dist/types/live_status.d.cts +16 -16
- package/dist/types/live_status.d.mts +16 -16
- package/dist/types/responses.d.cts +5 -1
- package/dist/types/responses.d.mts +5 -1
- package/dist/types/vehicle.d.cts +7 -1
- package/dist/types/vehicle.d.mts +7 -1
- package/dist/types.d.cts +3 -3
- package/dist/types.d.mts +3 -3
- package/dist/util.cjs +41 -0
- package/dist/util.d.cts +18 -0
- package/dist/util.d.mts +18 -0
- package/dist/util.mjs +40 -0
- package/dist/vehicle.cjs +79 -36
- package/dist/vehicle.d.cts +42 -26
- package/dist/vehicle.d.mts +42 -26
- package/dist/vehicle.mjs +80 -37
- package/dist/vehiclesigned.cjs +20 -5
- package/dist/vehiclesigned.d.cts +14 -5
- package/dist/vehiclesigned.d.mts +14 -5
- package/dist/vehiclesigned.mjs +20 -5
- package/dist/vehiclespecific.cjs +126 -15
- package/dist/vehiclespecific.d.cts +82 -18
- package/dist/vehiclespecific.d.mts +82 -18
- package/dist/vehiclespecific.mjs +126 -15
- package/package.json +70 -3
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/bluetooth/constants.ts
|
|
2
|
+
/** The vehicle's GATT service. It is never advertised, so never scan for it. */
|
|
3
|
+
const SERVICE_UUID = "00000211-b2d1-43f0-9b88-960cebf8b91e";
|
|
4
|
+
/** Client to car. Tesla's Go SDK names the two by the opposite view, hence these names. */
|
|
5
|
+
const VEHICLE_WRITE_UUID = "00000212-b2d1-43f0-9b88-960cebf8b91e";
|
|
6
|
+
/** Car to client. */
|
|
7
|
+
const VEHICLE_NOTIFY_UUID = "00000213-b2d1-43f0-9b88-960cebf8b91e";
|
|
8
|
+
/** Python's name for `VEHICLE_WRITE_UUID`. */
|
|
9
|
+
const WRITE_UUID = VEHICLE_WRITE_UUID;
|
|
10
|
+
/** Python's name for `VEHICLE_NOTIFY_UUID`. */
|
|
11
|
+
const READ_UUID = VEHICLE_NOTIFY_UUID;
|
|
12
|
+
/** The keepalive read target. */
|
|
13
|
+
const VERSION_UUID = "00000214-b2d1-43f0-9b88-960cebf8b91e";
|
|
14
|
+
/** GATT Device Name. */
|
|
15
|
+
const NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb";
|
|
16
|
+
/** GATT Appearance. */
|
|
17
|
+
const APPEARANCE_UUID = "00002a01-0000-1000-8000-00805f9b34fb";
|
|
18
|
+
/** Generic Access, for a backend that needs a service UUID to reach the two above. */
|
|
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
|
+
/** Largest frame accepted, length prefix included. Tesla's Go SDK allows 2 more bytes. */
|
|
23
|
+
const MAX_FRAME_SIZE = 1024;
|
|
24
|
+
/**
|
|
25
|
+
* Bytes per write when a connection does not say: the ATT default MTU of 23,
|
|
26
|
+
* less 3 of header. Safe on any link, and all Homey offers, as it hides the MTU.
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_MAX_WRITE_SIZE = 20;
|
|
29
|
+
/** A chunk after this much silence means the partial frame before it was abandoned. */
|
|
30
|
+
const STALE_CHUNK_TIMEOUT_MS = 1e3;
|
|
31
|
+
/** An idle held link drops at about 42 s on average; a read this often keeps it about 10 times longer. */
|
|
32
|
+
const DEFAULT_KEEPALIVE_INTERVAL_MS = 2e4;
|
|
33
|
+
/** Infotainment answers a handshake 4 to 16 s after an RKE wake. */
|
|
34
|
+
const DEFAULT_WAKE_TIMEOUT_MS = 3e4;
|
|
35
|
+
/**
|
|
36
|
+
* One infotainment readiness probe. A ready infotainment answers in 0.26 to 0.39 s.
|
|
37
|
+
* The window opens once the request is fully written, so a slow 20-byte link
|
|
38
|
+
* spends none of it writing. Raise it with `wake_probe_timeout_ms` if a link's
|
|
39
|
+
* replies are slower than this, as a probe that keeps timing out never proves
|
|
40
|
+
* the car is ready.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_WAKE_PROBE_TIMEOUT_MS = 1e3;
|
|
43
|
+
/** How often a wake is resent while infotainment stays silent: a first wake can be missed after a long sleep. */
|
|
44
|
+
const WAKE_RESEND_INTERVAL_MS = 3e3;
|
|
45
|
+
/** How long an awake car's infotainment may take to finish booting. */
|
|
46
|
+
const INFOTAINMENT_BOOT_TIMEOUT_MS = 2e4;
|
|
47
|
+
/** How long a charge-port opening has to show in charge state. It took about 1.4 s in live trials. */
|
|
48
|
+
const CHARGE_PORT_VERIFY_TIMEOUT_MS = 5e3;
|
|
49
|
+
/** Between charge-state polls, while waiting for readiness or for the latch. */
|
|
50
|
+
const CHARGE_PORT_VERIFY_INTERVAL_MS = 250;
|
|
51
|
+
/** How long `pair` waits for someone to tap a key card. */
|
|
52
|
+
const DEFAULT_PAIR_TIMEOUT_MS = 3e5;
|
|
53
|
+
/** Between `pair`'s checks of whether the key was accepted. */
|
|
54
|
+
const DEFAULT_PAIR_POLL_INTERVAL_MS = 5e3;
|
|
55
|
+
/** Connect attempts. Each is bounded by `DEFAULT_CONNECT_TIMEOUT_MS`. */
|
|
56
|
+
const DEFAULT_CONNECT_ATTEMPTS = 3;
|
|
57
|
+
/** One connect attempt. Python takes the same figure from `bleak-retry-connector`. */
|
|
58
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 2e4;
|
|
59
|
+
/** One scan. It must outlast the backend's window, which Homey fixes at about 5 s. */
|
|
60
|
+
const DEFAULT_SCAN_TIMEOUT_MS = 1e4;
|
|
61
|
+
/** Bound on tearing down a link: the failure behind it is what gets reported. */
|
|
62
|
+
const LINK_TEARDOWN_TIMEOUT_MS = 5e3;
|
|
63
|
+
/**
|
|
64
|
+
* One write call. Not in Python, whose backends bound their own writes. Homey's
|
|
65
|
+
* have no timeout and its link state can be stale, so a hung write must end
|
|
66
|
+
* here. A design choice. BLE-00 step 10 measures how long a failing write takes
|
|
67
|
+
* on Homey, which sets it.
|
|
68
|
+
*/
|
|
69
|
+
const GATT_WRITE_TIMEOUT_MS = 5e3;
|
|
70
|
+
/** One read call, other than a keepalive's. */
|
|
71
|
+
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 };
|
|
@@ -0,0 +1,292 @@
|
|
|
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;
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
}
|