tesla-fleet-api 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -1
- package/dist/_virtual/_rolldown/runtime.cjs +10 -0
- package/dist/bluetooth/adapters/shared.cjs +38 -0
- package/dist/bluetooth/adapters/shared.mjs +36 -0
- package/dist/bluetooth/async.cjs +121 -0
- package/dist/bluetooth/async.d.cts +10 -0
- package/dist/bluetooth/async.d.mts +10 -0
- package/dist/bluetooth/async.mjs +116 -0
- package/dist/bluetooth/broadcast.cjs +153 -0
- package/dist/bluetooth/broadcast.d.cts +59 -0
- package/dist/bluetooth/broadcast.d.mts +59 -0
- package/dist/bluetooth/broadcast.mjs +151 -0
- package/dist/bluetooth/constants.cjs +96 -0
- package/dist/bluetooth/constants.d.cts +68 -0
- package/dist/bluetooth/constants.d.mts +68 -0
- package/dist/bluetooth/constants.mjs +70 -0
- package/dist/bluetooth/framing.cjs +125 -0
- package/dist/bluetooth/framing.d.cts +45 -0
- package/dist/bluetooth/framing.d.mts +45 -0
- package/dist/bluetooth/framing.mjs +122 -0
- package/dist/bluetooth/homey.cjs +165 -0
- package/dist/bluetooth/homey.d.cts +84 -0
- package/dist/bluetooth/homey.d.mts +84 -0
- package/dist/bluetooth/homey.mjs +159 -0
- package/dist/bluetooth/names.cjs +24 -0
- package/dist/bluetooth/names.d.cts +10 -0
- package/dist/bluetooth/names.d.mts +10 -0
- package/dist/bluetooth/names.mjs +20 -0
- package/dist/bluetooth/noble.cjs +178 -0
- package/dist/bluetooth/noble.d.cts +58 -0
- package/dist/bluetooth/noble.d.mts +58 -0
- package/dist/bluetooth/noble.mjs +177 -0
- package/dist/bluetooth/streamglue.cjs +157 -0
- package/dist/bluetooth/streamglue.d.cts +37 -0
- package/dist/bluetooth/streamglue.d.mts +37 -0
- package/dist/bluetooth/streamglue.mjs +154 -0
- package/dist/bluetooth/teslabluetooth.cjs +67 -0
- package/dist/bluetooth/teslabluetooth.d.cts +41 -0
- package/dist/bluetooth/teslabluetooth.d.mts +41 -0
- package/dist/bluetooth/teslabluetooth.mjs +66 -0
- package/dist/bluetooth/transport.cjs +10 -0
- package/dist/bluetooth/transport.d.cts +97 -0
- package/dist/bluetooth/transport.d.mts +97 -0
- package/dist/bluetooth/transport.mjs +10 -0
- package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
- package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
- package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
- package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
- package/dist/bluetooth/verify.cjs +93 -0
- package/dist/bluetooth/verify.d.cts +35 -0
- package/dist/bluetooth/verify.d.mts +35 -0
- package/dist/bluetooth/verify.mjs +91 -0
- package/dist/bluetooth.cjs +52 -0
- package/dist/bluetooth.d.cts +10 -0
- package/dist/bluetooth.d.mts +10 -0
- package/dist/bluetooth.mjs +9 -0
- package/dist/commands.cjs +1080 -85
- package/dist/commands.d.cts +585 -54
- package/dist/commands.d.mts +583 -51
- package/dist/commands.mjs +1084 -93
- package/dist/const.cjs +22 -0
- package/dist/const.d.cts +21 -0
- package/dist/const.d.mts +21 -0
- package/dist/const.mjs +20 -1
- package/dist/energy.cjs +24 -19
- package/dist/energy.d.cts +21 -15
- package/dist/energy.d.mts +21 -15
- package/dist/energy.mjs +24 -19
- package/dist/energyisland.cjs +38 -0
- package/dist/energyisland.mjs +37 -0
- package/dist/energyparsers.cjs +118 -0
- package/dist/energyparsers.d.cts +39 -0
- package/dist/energyparsers.d.mts +39 -0
- package/dist/energyparsers.mjs +116 -0
- package/dist/energyspecific.cjs +375 -17
- package/dist/energyspecific.d.cts +199 -19
- package/dist/energyspecific.d.mts +197 -16
- package/dist/energyspecific.mjs +370 -18
- package/dist/exceptions.cjs +3 -2
- package/dist/exceptions.d.cts +3 -2
- package/dist/exceptions.d.mts +3 -2
- package/dist/exceptions.mjs +3 -2
- package/dist/funnel.cjs +297 -0
- package/dist/funnel.d.cts +118 -0
- package/dist/funnel.d.mts +118 -0
- package/dist/funnel.mjs +292 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +22 -8
- package/dist/index.d.mts +22 -8
- package/dist/index.mjs +19 -6
- package/dist/log.cjs +75 -0
- package/dist/log.d.cts +4 -0
- package/dist/log.d.mts +4 -0
- package/dist/log.mjs +71 -0
- package/dist/oauth.cjs +143 -0
- package/dist/oauth.d.cts +69 -0
- package/dist/oauth.d.mts +69 -0
- package/dist/oauth.mjs +143 -0
- package/dist/powerwall/client.cjs +818 -0
- package/dist/powerwall/client.d.cts +297 -0
- package/dist/powerwall/client.d.mts +297 -0
- package/dist/powerwall/client.mjs +807 -0
- package/dist/powerwall/components.cjs +188 -0
- package/dist/powerwall/components.d.cts +45 -0
- package/dist/powerwall/components.d.mts +45 -0
- package/dist/powerwall/components.mjs +182 -0
- package/dist/powerwall/energysite.cjs +216 -0
- package/dist/powerwall/energysite.d.cts +104 -0
- package/dist/powerwall/energysite.d.mts +104 -0
- package/dist/powerwall/energysite.mjs +214 -0
- package/dist/powerwall/exceptions.cjs +59 -0
- package/dist/powerwall/exceptions.d.cts +38 -0
- package/dist/powerwall/exceptions.d.mts +38 -0
- package/dist/powerwall/exceptions.mjs +51 -0
- package/dist/powerwall/gateway.cjs +102 -0
- package/dist/powerwall/gateway.d.cts +57 -0
- package/dist/powerwall/gateway.d.mts +57 -0
- package/dist/powerwall/gateway.mjs +101 -0
- package/dist/powerwall/merge.cjs +163 -0
- package/dist/powerwall/merge.d.cts +63 -0
- package/dist/powerwall/merge.d.mts +63 -0
- package/dist/powerwall/merge.mjs +159 -0
- package/dist/powerwall/models.d.cts +95 -0
- package/dist/powerwall/models.d.mts +95 -0
- package/dist/powerwall/queries.cjs +17 -0
- package/dist/powerwall/queries.mjs +10 -0
- package/dist/powerwall/transport.cjs +232 -0
- package/dist/powerwall/transport.mjs +227 -0
- package/dist/powerwall.cjs +47 -0
- package/dist/powerwall.d.cts +9 -0
- package/dist/powerwall.d.mts +9 -0
- package/dist/powerwall.mjs +8 -0
- package/dist/router/base.cjs +203 -0
- package/dist/router/base.d.cts +112 -0
- package/dist/router/base.d.mts +112 -0
- package/dist/router/base.mjs +200 -0
- package/dist/router/energysite.cjs +65 -0
- package/dist/router/energysite.d.cts +47 -0
- package/dist/router/energysite.d.mts +47 -0
- package/dist/router/energysite.mjs +60 -0
- package/dist/router.cjs +13 -0
- package/dist/router.d.cts +3 -1
- package/dist/router.d.mts +3 -1
- package/dist/router.mjs +3 -1
- package/dist/signing/crypto.cjs +28 -0
- package/dist/signing/crypto.mjs +26 -1
- package/dist/signing/energy.cjs +123 -0
- package/dist/signing/energy.d.cts +45 -0
- package/dist/signing/energy.d.mts +45 -0
- package/dist/signing/energy.mjs +120 -0
- package/dist/signing/jws.cjs +229 -0
- package/dist/signing/jws.d.cts +45 -0
- package/dist/signing/jws.d.mts +45 -0
- package/dist/signing/jws.mjs +211 -0
- package/dist/signing/keys.cjs +191 -0
- package/dist/signing/keys.d.cts +21 -0
- package/dist/signing/keys.d.mts +21 -0
- package/dist/signing/keys.mjs +182 -0
- package/dist/signing/session.cjs +104 -30
- package/dist/signing/session.d.cts +60 -23
- package/dist/signing/session.d.mts +60 -23
- package/dist/signing/session.mjs +105 -31
- package/dist/tariff.cjs +27 -0
- package/dist/tariff.d.cts +11 -0
- package/dist/tariff.d.mts +11 -0
- package/dist/tariff.mjs +27 -1
- package/dist/teslafleetapi.cjs +256 -55
- package/dist/teslafleetapi.d.cts +110 -14
- package/dist/teslafleetapi.d.mts +110 -14
- package/dist/teslafleetapi.mjs +258 -57
- package/dist/teslemetry.cjs +152 -5
- package/dist/teslemetry.d.cts +95 -5
- package/dist/teslemetry.d.mts +93 -3
- package/dist/teslemetry.mjs +151 -6
- package/dist/teslemetryenergy.cjs +287 -0
- package/dist/teslemetryenergy.d.cts +154 -0
- package/dist/teslemetryenergy.d.mts +154 -0
- package/dist/teslemetryenergy.mjs +282 -0
- package/dist/teslemetryvehicle.cjs +407 -0
- package/dist/teslemetryvehicle.d.cts +238 -0
- package/dist/teslemetryvehicle.d.mts +238 -0
- package/dist/teslemetryvehicle.mjs +405 -0
- package/dist/tessie.cjs +42 -5
- package/dist/tessie.d.cts +51 -5
- package/dist/tessie.d.mts +49 -3
- package/dist/tessie.mjs +42 -5
- package/dist/tessievehicle.cjs +514 -0
- package/dist/tessievehicle.d.cts +333 -0
- package/dist/tessievehicle.d.mts +333 -0
- package/dist/tessievehicle.mjs +513 -0
- package/dist/types/live_status.d.cts +16 -16
- package/dist/types/live_status.d.mts +16 -16
- package/dist/types/responses.d.cts +5 -1
- package/dist/types/responses.d.mts +5 -1
- package/dist/types/vehicle.d.cts +7 -1
- package/dist/types/vehicle.d.mts +7 -1
- package/dist/types.d.cts +3 -3
- package/dist/types.d.mts +3 -3
- package/dist/util.cjs +41 -0
- package/dist/util.d.cts +18 -0
- package/dist/util.d.mts +18 -0
- package/dist/util.mjs +40 -0
- package/dist/vehicle.cjs +79 -36
- package/dist/vehicle.d.cts +42 -26
- package/dist/vehicle.d.mts +42 -26
- package/dist/vehicle.mjs +80 -37
- package/dist/vehiclesigned.cjs +20 -5
- package/dist/vehiclesigned.d.cts +14 -5
- package/dist/vehiclesigned.d.mts +14 -5
- package/dist/vehiclesigned.mjs +20 -5
- package/dist/vehiclespecific.cjs +126 -15
- package/dist/vehiclespecific.d.cts +82 -18
- package/dist/vehiclespecific.d.mts +82 -18
- package/dist/vehiclespecific.mjs +126 -15
- package/package.json +56 -3
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
/** Largest frame accepted, length prefix included. Tesla's Go SDK allows 2 more bytes. */
|
|
21
|
+
const MAX_FRAME_SIZE = 1024;
|
|
22
|
+
/**
|
|
23
|
+
* Bytes per write when a connection does not say: the ATT default MTU of 23,
|
|
24
|
+
* less 3 of header. Safe on any link, and all Homey offers, as it hides the MTU.
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_MAX_WRITE_SIZE = 20;
|
|
27
|
+
/** A chunk after this much silence means the partial frame before it was abandoned. */
|
|
28
|
+
const STALE_CHUNK_TIMEOUT_MS = 1e3;
|
|
29
|
+
/** An idle held link drops at about 42 s on average; a read this often keeps it about 10 times longer. */
|
|
30
|
+
const DEFAULT_KEEPALIVE_INTERVAL_MS = 2e4;
|
|
31
|
+
/** Infotainment answers a handshake 4 to 16 s after an RKE wake. */
|
|
32
|
+
const DEFAULT_WAKE_TIMEOUT_MS = 3e4;
|
|
33
|
+
/**
|
|
34
|
+
* One infotainment readiness probe. A ready infotainment answers in 0.26 to 0.39 s.
|
|
35
|
+
* The window opens once the request is fully written, so a slow 20-byte link
|
|
36
|
+
* spends none of it writing. Raise it with `wake_probe_timeout_ms` if a link's
|
|
37
|
+
* replies are slower than this, as a probe that keeps timing out never proves
|
|
38
|
+
* the car is ready.
|
|
39
|
+
*/
|
|
40
|
+
const DEFAULT_WAKE_PROBE_TIMEOUT_MS = 1e3;
|
|
41
|
+
/** How often a wake is resent while infotainment stays silent: a first wake can be missed after a long sleep. */
|
|
42
|
+
const WAKE_RESEND_INTERVAL_MS = 3e3;
|
|
43
|
+
/** How long an awake car's infotainment may take to finish booting. */
|
|
44
|
+
const INFOTAINMENT_BOOT_TIMEOUT_MS = 2e4;
|
|
45
|
+
/** How long a charge-port opening has to show in charge state. It took about 1.4 s in live trials. */
|
|
46
|
+
const CHARGE_PORT_VERIFY_TIMEOUT_MS = 5e3;
|
|
47
|
+
/** Between charge-state polls, while waiting for readiness or for the latch. */
|
|
48
|
+
const CHARGE_PORT_VERIFY_INTERVAL_MS = 250;
|
|
49
|
+
/** How long `pair` waits for someone to tap a key card. */
|
|
50
|
+
const DEFAULT_PAIR_TIMEOUT_MS = 3e5;
|
|
51
|
+
/** Between `pair`'s checks of whether the key was accepted. */
|
|
52
|
+
const DEFAULT_PAIR_POLL_INTERVAL_MS = 5e3;
|
|
53
|
+
/** Connect attempts. Each is bounded by `DEFAULT_CONNECT_TIMEOUT_MS`. */
|
|
54
|
+
const DEFAULT_CONNECT_ATTEMPTS = 3;
|
|
55
|
+
/** One connect attempt. Python takes the same figure from `bleak-retry-connector`. */
|
|
56
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 2e4;
|
|
57
|
+
/** One scan. It must outlast the backend's window, which Homey fixes at about 10 s: its adapter sets its own bound. */
|
|
58
|
+
const DEFAULT_SCAN_TIMEOUT_MS = 1e4;
|
|
59
|
+
/** Bound on tearing down a link: the failure behind it is what gets reported. */
|
|
60
|
+
const LINK_TEARDOWN_TIMEOUT_MS = 5e3;
|
|
61
|
+
/**
|
|
62
|
+
* One write call. Not in Python, whose backends bound their own writes. Homey's
|
|
63
|
+
* have no timeout and its link state can be stale, so a hung write must end
|
|
64
|
+
* here. A design choice. BLE-00 step 10 measures how long a failing write takes
|
|
65
|
+
* on Homey, which sets it.
|
|
66
|
+
*/
|
|
67
|
+
const GATT_WRITE_TIMEOUT_MS = 5e3;
|
|
68
|
+
/** One read call, other than a keepalive's. */
|
|
69
|
+
const GATT_READ_TIMEOUT_MS = 5e3;
|
|
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 };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require("./constants.cjs");
|
|
2
|
+
let _teslemetry_tesla_protocol_command_universal_message = require("@teslemetry/tesla-protocol/command/universal_message");
|
|
3
|
+
let _teslemetry_tesla_protocol_command_vcsec = require("@teslemetry/tesla-protocol/command/vcsec");
|
|
4
|
+
// src/bluetooth/framing.ts
|
|
5
|
+
/** Prefix a payload with its big-endian 2-byte length, which counts the payload only. */
|
|
6
|
+
function prependLength(message) {
|
|
7
|
+
const header = Buffer.alloc(2);
|
|
8
|
+
header.writeUInt16BE(message.length);
|
|
9
|
+
return Buffer.concat([header, message]);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Cut a frame into the writes that carry it.
|
|
13
|
+
* @param maxWriteSize Largest bytes per write. `null` means the backend accepts a full frame, as Python's do.
|
|
14
|
+
*/
|
|
15
|
+
function splitFrame(frame, maxWriteSize) {
|
|
16
|
+
if (maxWriteSize === null) return [frame];
|
|
17
|
+
if (!Number.isInteger(maxWriteSize) || maxWriteSize <= 0) throw new RangeError(`maxWriteSize must be a positive integer, got ${maxWriteSize}.`);
|
|
18
|
+
const chunks = [];
|
|
19
|
+
for (let start = 0; start < frame.length; start += maxWriteSize) chunks.push(frame.subarray(start, start + maxWriteSize));
|
|
20
|
+
return chunks;
|
|
21
|
+
}
|
|
22
|
+
/** Whether a decoded `RoutableMessage` holds nothing: ts-proto's shape for "no known field". */
|
|
23
|
+
function hasNoKnownField(message) {
|
|
24
|
+
return message.toDestination === void 0 && message.fromDestination === void 0 && message.protobufMessageAsBytes === void 0 && message.sessionInfoRequest === void 0 && message.sessionInfo === void 0 && message.signatureData === void 0 && message.signedMessageStatus === void 0 && message.messageFrame === void 0 && message.requestUuid.length === 0 && message.uuid.length === 0 && message.flags === 0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse one reassembled frame, wrapping a bare VCSEC message.
|
|
28
|
+
*
|
|
29
|
+
* On a fresh link the car sends `FromVCSECMessage` frames, such as a repeating
|
|
30
|
+
* `authenticationRequest`, with no `RoutableMessage` around them. None of its
|
|
31
|
+
* field numbers exist on `RoutableMessage`, so one parses as a message with no
|
|
32
|
+
* known field. It is wrapped as an unaddressed VCSEC message, so it reaches the
|
|
33
|
+
* broadcast listeners as a wrapped one would.
|
|
34
|
+
* @throws When the frame is not a `RoutableMessage`.
|
|
35
|
+
*/
|
|
36
|
+
function parseFrame(payload) {
|
|
37
|
+
const message = _teslemetry_tesla_protocol_command_universal_message.RoutableMessage.decode(payload);
|
|
38
|
+
if (payload.length === 0 || !hasNoKnownField(message)) return message;
|
|
39
|
+
let vcsec;
|
|
40
|
+
try {
|
|
41
|
+
vcsec = _teslemetry_tesla_protocol_command_vcsec.FromVCSECMessage.decode(payload);
|
|
42
|
+
} catch {
|
|
43
|
+
return message;
|
|
44
|
+
}
|
|
45
|
+
if (!Object.values(vcsec).some((field) => field !== void 0)) return message;
|
|
46
|
+
return _teslemetry_tesla_protocol_command_universal_message.RoutableMessage.create({
|
|
47
|
+
fromDestination: { domain: _teslemetry_tesla_protocol_command_universal_message.Domain.DOMAIN_VEHICLE_SECURITY },
|
|
48
|
+
protobufMessageAsBytes: payload
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Reassembles BLE notifications into length-prefixed messages.
|
|
53
|
+
*
|
|
54
|
+
* One notification can hold part of a message, one message, or several. A
|
|
55
|
+
* message that cannot be decoded drops the current notification and resyncs at
|
|
56
|
+
* the next one. A partial message is also dropped if its next chunk does not
|
|
57
|
+
* arrive within `STALE_CHUNK_TIMEOUT_MS`.
|
|
58
|
+
*/
|
|
59
|
+
var ReassemblingBuffer = class {
|
|
60
|
+
callback;
|
|
61
|
+
buffer = Buffer.alloc(0);
|
|
62
|
+
expectedLength = null;
|
|
63
|
+
/** Where each notification still in `buffer` starts: the only places a resync can land. */
|
|
64
|
+
packetStarts = [];
|
|
65
|
+
lastChunkAt = null;
|
|
66
|
+
now;
|
|
67
|
+
/**
|
|
68
|
+
* @param callback Called with each parsed message. Its errors are not caught.
|
|
69
|
+
* @param now The monotonic clock, in milliseconds. Injectable for tests.
|
|
70
|
+
*/
|
|
71
|
+
constructor(callback, now = () => performance.now()) {
|
|
72
|
+
this.callback = callback;
|
|
73
|
+
this.now = now;
|
|
74
|
+
}
|
|
75
|
+
/** Take one notification and emit every message it completes. Malformed bytes never throw. */
|
|
76
|
+
receive(chunk) {
|
|
77
|
+
const now = this.now();
|
|
78
|
+
if (this.buffer.length > 0 && this.lastChunkAt !== null && now - this.lastChunkAt > 1e3) {
|
|
79
|
+
this.buffer = Buffer.alloc(0);
|
|
80
|
+
this.expectedLength = null;
|
|
81
|
+
this.packetStarts = [];
|
|
82
|
+
}
|
|
83
|
+
this.lastChunkAt = now;
|
|
84
|
+
this.packetStarts.push(this.buffer.length);
|
|
85
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
86
|
+
for (;;) {
|
|
87
|
+
if (this.expectedLength === null && this.buffer.length >= 2) this.expectedLength = this.buffer.readUInt16BE(0) + 2;
|
|
88
|
+
if (this.expectedLength === null) return;
|
|
89
|
+
if (this.expectedLength > 1024) {
|
|
90
|
+
this.discardPacket();
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (this.buffer.length < this.expectedLength) return;
|
|
94
|
+
let message;
|
|
95
|
+
try {
|
|
96
|
+
message = parseFrame(this.buffer.subarray(2, this.expectedLength));
|
|
97
|
+
} catch {
|
|
98
|
+
this.discardPacket();
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const consumed = this.expectedLength;
|
|
102
|
+
this.buffer = this.buffer.subarray(consumed);
|
|
103
|
+
this.packetStarts = this.packetStarts.filter((start) => start >= consumed).map((start) => start - consumed);
|
|
104
|
+
this.expectedLength = null;
|
|
105
|
+
this.callback(message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Drop the current notification and resync at the next, if there is one. */
|
|
109
|
+
discardPacket() {
|
|
110
|
+
this.packetStarts.shift();
|
|
111
|
+
const next = this.packetStarts[0];
|
|
112
|
+
if (next === void 0) {
|
|
113
|
+
this.buffer = Buffer.alloc(0);
|
|
114
|
+
this.packetStarts = [];
|
|
115
|
+
} else {
|
|
116
|
+
this.buffer = this.buffer.subarray(next);
|
|
117
|
+
this.packetStarts = this.packetStarts.map((start) => start - next);
|
|
118
|
+
}
|
|
119
|
+
this.expectedLength = null;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
exports.ReassemblingBuffer = ReassemblingBuffer;
|
|
123
|
+
exports.parseFrame = parseFrame;
|
|
124
|
+
exports.prependLength = prependLength;
|
|
125
|
+
exports.splitFrame = splitFrame;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RoutableMessage } from "@teslemetry/tesla-protocol/command/universal_message";
|
|
2
|
+
/** Prefix a payload with its big-endian 2-byte length, which counts the payload only. */
|
|
3
|
+
export declare function prependLength(message: Uint8Array): Buffer;
|
|
4
|
+
/**
|
|
5
|
+
* Cut a frame into the writes that carry it.
|
|
6
|
+
* @param maxWriteSize Largest bytes per write. `null` means the backend accepts a full frame, as Python's do.
|
|
7
|
+
*/
|
|
8
|
+
export declare function splitFrame(frame: Uint8Array, maxWriteSize: number | null): Uint8Array[];
|
|
9
|
+
/**
|
|
10
|
+
* Parse one reassembled frame, wrapping a bare VCSEC message.
|
|
11
|
+
*
|
|
12
|
+
* On a fresh link the car sends `FromVCSECMessage` frames, such as a repeating
|
|
13
|
+
* `authenticationRequest`, with no `RoutableMessage` around them. None of its
|
|
14
|
+
* field numbers exist on `RoutableMessage`, so one parses as a message with no
|
|
15
|
+
* known field. It is wrapped as an unaddressed VCSEC message, so it reaches the
|
|
16
|
+
* broadcast listeners as a wrapped one would.
|
|
17
|
+
* @throws When the frame is not a `RoutableMessage`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseFrame(payload: Uint8Array): RoutableMessage;
|
|
20
|
+
/**
|
|
21
|
+
* Reassembles BLE notifications into length-prefixed messages.
|
|
22
|
+
*
|
|
23
|
+
* One notification can hold part of a message, one message, or several. A
|
|
24
|
+
* message that cannot be decoded drops the current notification and resyncs at
|
|
25
|
+
* the next one. A partial message is also dropped if its next chunk does not
|
|
26
|
+
* arrive within `STALE_CHUNK_TIMEOUT_MS`.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ReassemblingBuffer {
|
|
29
|
+
private readonly callback;
|
|
30
|
+
private buffer;
|
|
31
|
+
private expectedLength;
|
|
32
|
+
/** Where each notification still in `buffer` starts: the only places a resync can land. */
|
|
33
|
+
private packetStarts;
|
|
34
|
+
private lastChunkAt;
|
|
35
|
+
private readonly now;
|
|
36
|
+
/**
|
|
37
|
+
* @param callback Called with each parsed message. Its errors are not caught.
|
|
38
|
+
* @param now The monotonic clock, in milliseconds. Injectable for tests.
|
|
39
|
+
*/
|
|
40
|
+
constructor(callback: (message: RoutableMessage) => void, now?: () => number);
|
|
41
|
+
/** Take one notification and emit every message it completes. Malformed bytes never throw. */
|
|
42
|
+
receive(chunk: Uint8Array): void;
|
|
43
|
+
/** Drop the current notification and resync at the next, if there is one. */
|
|
44
|
+
private discardPacket;
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RoutableMessage } from "@teslemetry/tesla-protocol/command/universal_message";
|
|
2
|
+
/** Prefix a payload with its big-endian 2-byte length, which counts the payload only. */
|
|
3
|
+
export declare function prependLength(message: Uint8Array): Buffer;
|
|
4
|
+
/**
|
|
5
|
+
* Cut a frame into the writes that carry it.
|
|
6
|
+
* @param maxWriteSize Largest bytes per write. `null` means the backend accepts a full frame, as Python's do.
|
|
7
|
+
*/
|
|
8
|
+
export declare function splitFrame(frame: Uint8Array, maxWriteSize: number | null): Uint8Array[];
|
|
9
|
+
/**
|
|
10
|
+
* Parse one reassembled frame, wrapping a bare VCSEC message.
|
|
11
|
+
*
|
|
12
|
+
* On a fresh link the car sends `FromVCSECMessage` frames, such as a repeating
|
|
13
|
+
* `authenticationRequest`, with no `RoutableMessage` around them. None of its
|
|
14
|
+
* field numbers exist on `RoutableMessage`, so one parses as a message with no
|
|
15
|
+
* known field. It is wrapped as an unaddressed VCSEC message, so it reaches the
|
|
16
|
+
* broadcast listeners as a wrapped one would.
|
|
17
|
+
* @throws When the frame is not a `RoutableMessage`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseFrame(payload: Uint8Array): RoutableMessage;
|
|
20
|
+
/**
|
|
21
|
+
* Reassembles BLE notifications into length-prefixed messages.
|
|
22
|
+
*
|
|
23
|
+
* One notification can hold part of a message, one message, or several. A
|
|
24
|
+
* message that cannot be decoded drops the current notification and resyncs at
|
|
25
|
+
* the next one. A partial message is also dropped if its next chunk does not
|
|
26
|
+
* arrive within `STALE_CHUNK_TIMEOUT_MS`.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ReassemblingBuffer {
|
|
29
|
+
private readonly callback;
|
|
30
|
+
private buffer;
|
|
31
|
+
private expectedLength;
|
|
32
|
+
/** Where each notification still in `buffer` starts: the only places a resync can land. */
|
|
33
|
+
private packetStarts;
|
|
34
|
+
private lastChunkAt;
|
|
35
|
+
private readonly now;
|
|
36
|
+
/**
|
|
37
|
+
* @param callback Called with each parsed message. Its errors are not caught.
|
|
38
|
+
* @param now The monotonic clock, in milliseconds. Injectable for tests.
|
|
39
|
+
*/
|
|
40
|
+
constructor(callback: (message: RoutableMessage) => void, now?: () => number);
|
|
41
|
+
/** Take one notification and emit every message it completes. Malformed bytes never throw. */
|
|
42
|
+
receive(chunk: Uint8Array): void;
|
|
43
|
+
/** Drop the current notification and resync at the next, if there is one. */
|
|
44
|
+
private discardPacket;
|
|
45
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import "./constants.mjs";
|
|
2
|
+
import { Domain, RoutableMessage } from "@teslemetry/tesla-protocol/command/universal_message";
|
|
3
|
+
import { FromVCSECMessage } from "@teslemetry/tesla-protocol/command/vcsec";
|
|
4
|
+
// src/bluetooth/framing.ts
|
|
5
|
+
/** Prefix a payload with its big-endian 2-byte length, which counts the payload only. */
|
|
6
|
+
function prependLength(message) {
|
|
7
|
+
const header = Buffer.alloc(2);
|
|
8
|
+
header.writeUInt16BE(message.length);
|
|
9
|
+
return Buffer.concat([header, message]);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Cut a frame into the writes that carry it.
|
|
13
|
+
* @param maxWriteSize Largest bytes per write. `null` means the backend accepts a full frame, as Python's do.
|
|
14
|
+
*/
|
|
15
|
+
function splitFrame(frame, maxWriteSize) {
|
|
16
|
+
if (maxWriteSize === null) return [frame];
|
|
17
|
+
if (!Number.isInteger(maxWriteSize) || maxWriteSize <= 0) throw new RangeError(`maxWriteSize must be a positive integer, got ${maxWriteSize}.`);
|
|
18
|
+
const chunks = [];
|
|
19
|
+
for (let start = 0; start < frame.length; start += maxWriteSize) chunks.push(frame.subarray(start, start + maxWriteSize));
|
|
20
|
+
return chunks;
|
|
21
|
+
}
|
|
22
|
+
/** Whether a decoded `RoutableMessage` holds nothing: ts-proto's shape for "no known field". */
|
|
23
|
+
function hasNoKnownField(message) {
|
|
24
|
+
return message.toDestination === void 0 && message.fromDestination === void 0 && message.protobufMessageAsBytes === void 0 && message.sessionInfoRequest === void 0 && message.sessionInfo === void 0 && message.signatureData === void 0 && message.signedMessageStatus === void 0 && message.messageFrame === void 0 && message.requestUuid.length === 0 && message.uuid.length === 0 && message.flags === 0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse one reassembled frame, wrapping a bare VCSEC message.
|
|
28
|
+
*
|
|
29
|
+
* On a fresh link the car sends `FromVCSECMessage` frames, such as a repeating
|
|
30
|
+
* `authenticationRequest`, with no `RoutableMessage` around them. None of its
|
|
31
|
+
* field numbers exist on `RoutableMessage`, so one parses as a message with no
|
|
32
|
+
* known field. It is wrapped as an unaddressed VCSEC message, so it reaches the
|
|
33
|
+
* broadcast listeners as a wrapped one would.
|
|
34
|
+
* @throws When the frame is not a `RoutableMessage`.
|
|
35
|
+
*/
|
|
36
|
+
function parseFrame(payload) {
|
|
37
|
+
const message = RoutableMessage.decode(payload);
|
|
38
|
+
if (payload.length === 0 || !hasNoKnownField(message)) return message;
|
|
39
|
+
let vcsec;
|
|
40
|
+
try {
|
|
41
|
+
vcsec = FromVCSECMessage.decode(payload);
|
|
42
|
+
} catch {
|
|
43
|
+
return message;
|
|
44
|
+
}
|
|
45
|
+
if (!Object.values(vcsec).some((field) => field !== void 0)) return message;
|
|
46
|
+
return RoutableMessage.create({
|
|
47
|
+
fromDestination: { domain: Domain.DOMAIN_VEHICLE_SECURITY },
|
|
48
|
+
protobufMessageAsBytes: payload
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Reassembles BLE notifications into length-prefixed messages.
|
|
53
|
+
*
|
|
54
|
+
* One notification can hold part of a message, one message, or several. A
|
|
55
|
+
* message that cannot be decoded drops the current notification and resyncs at
|
|
56
|
+
* the next one. A partial message is also dropped if its next chunk does not
|
|
57
|
+
* arrive within `STALE_CHUNK_TIMEOUT_MS`.
|
|
58
|
+
*/
|
|
59
|
+
var ReassemblingBuffer = class {
|
|
60
|
+
callback;
|
|
61
|
+
buffer = Buffer.alloc(0);
|
|
62
|
+
expectedLength = null;
|
|
63
|
+
/** Where each notification still in `buffer` starts: the only places a resync can land. */
|
|
64
|
+
packetStarts = [];
|
|
65
|
+
lastChunkAt = null;
|
|
66
|
+
now;
|
|
67
|
+
/**
|
|
68
|
+
* @param callback Called with each parsed message. Its errors are not caught.
|
|
69
|
+
* @param now The monotonic clock, in milliseconds. Injectable for tests.
|
|
70
|
+
*/
|
|
71
|
+
constructor(callback, now = () => performance.now()) {
|
|
72
|
+
this.callback = callback;
|
|
73
|
+
this.now = now;
|
|
74
|
+
}
|
|
75
|
+
/** Take one notification and emit every message it completes. Malformed bytes never throw. */
|
|
76
|
+
receive(chunk) {
|
|
77
|
+
const now = this.now();
|
|
78
|
+
if (this.buffer.length > 0 && this.lastChunkAt !== null && now - this.lastChunkAt > 1e3) {
|
|
79
|
+
this.buffer = Buffer.alloc(0);
|
|
80
|
+
this.expectedLength = null;
|
|
81
|
+
this.packetStarts = [];
|
|
82
|
+
}
|
|
83
|
+
this.lastChunkAt = now;
|
|
84
|
+
this.packetStarts.push(this.buffer.length);
|
|
85
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
86
|
+
for (;;) {
|
|
87
|
+
if (this.expectedLength === null && this.buffer.length >= 2) this.expectedLength = this.buffer.readUInt16BE(0) + 2;
|
|
88
|
+
if (this.expectedLength === null) return;
|
|
89
|
+
if (this.expectedLength > 1024) {
|
|
90
|
+
this.discardPacket();
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (this.buffer.length < this.expectedLength) return;
|
|
94
|
+
let message;
|
|
95
|
+
try {
|
|
96
|
+
message = parseFrame(this.buffer.subarray(2, this.expectedLength));
|
|
97
|
+
} catch {
|
|
98
|
+
this.discardPacket();
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const consumed = this.expectedLength;
|
|
102
|
+
this.buffer = this.buffer.subarray(consumed);
|
|
103
|
+
this.packetStarts = this.packetStarts.filter((start) => start >= consumed).map((start) => start - consumed);
|
|
104
|
+
this.expectedLength = null;
|
|
105
|
+
this.callback(message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Drop the current notification and resync at the next, if there is one. */
|
|
109
|
+
discardPacket() {
|
|
110
|
+
this.packetStarts.shift();
|
|
111
|
+
const next = this.packetStarts[0];
|
|
112
|
+
if (next === void 0) {
|
|
113
|
+
this.buffer = Buffer.alloc(0);
|
|
114
|
+
this.packetStarts = [];
|
|
115
|
+
} else {
|
|
116
|
+
this.buffer = this.buffer.subarray(next);
|
|
117
|
+
this.packetStarts = this.packetStarts.map((start) => start - next);
|
|
118
|
+
}
|
|
119
|
+
this.expectedLength = null;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
export { ReassemblingBuffer, parseFrame, prependLength, splitFrame };
|
|
@@ -0,0 +1,165 @@
|
|
|
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/homey.ts
|
|
7
|
+
/**
|
|
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.
|
|
13
|
+
*/
|
|
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;
|
|
21
|
+
/**
|
|
22
|
+
* For `connect_timeout_ms` and `max_connect_attempts`, not applied by this
|
|
23
|
+
* adapter: `discover()` caches for 30 s, so a fast retry sees old data, and
|
|
24
|
+
* these fit Homey's 9 s action window. BLE-00 step 11 sets both, against its
|
|
25
|
+
* target of a connect under 4 s in 9 of 10. A flow with no such window should
|
|
26
|
+
* keep the library's defaults until then, as a link slower than this never lands.
|
|
27
|
+
*/
|
|
28
|
+
const HOMEY_VEHICLE_OPTIONS = {
|
|
29
|
+
connect_timeout_ms: 4e3,
|
|
30
|
+
max_connect_attempts: 1
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Bound on discovery and subscribing once a link is up. Homey has no timeout of
|
|
34
|
+
* its own, and if these hang, nothing else could release the vehicle's slot.
|
|
35
|
+
* BLE-00 steps 3 and 4 show what a slow link takes.
|
|
36
|
+
*/
|
|
37
|
+
const HOMEY_SETUP_TIMEOUT_MS = require_constants.DEFAULT_CONNECT_TIMEOUT_MS;
|
|
38
|
+
/** Bound on unsubscribing, which must not keep `disconnect` from running on a dead link. BLE-00 step 10. */
|
|
39
|
+
const HOMEY_UNSUBSCRIBE_TIMEOUT_MS = 1e3;
|
|
40
|
+
function find(characteristics, uuid) {
|
|
41
|
+
return characteristics.find((characteristic) => require_shared.compactUuid(characteristic.uuid) === require_shared.compactUuid(uuid));
|
|
42
|
+
}
|
|
43
|
+
var HomeyConnection = class {
|
|
44
|
+
id;
|
|
45
|
+
handlers;
|
|
46
|
+
maxWriteSize;
|
|
47
|
+
claims;
|
|
48
|
+
lost = false;
|
|
49
|
+
closed = false;
|
|
50
|
+
closing;
|
|
51
|
+
peripheral;
|
|
52
|
+
characteristics = [];
|
|
53
|
+
genericAccess;
|
|
54
|
+
subscribed = false;
|
|
55
|
+
onDisconnect = () => {
|
|
56
|
+
this.lost = true;
|
|
57
|
+
if (!this.closed) this.handlers.onDisconnect();
|
|
58
|
+
};
|
|
59
|
+
constructor(id, handlers, maxWriteSize, claims) {
|
|
60
|
+
this.id = id;
|
|
61
|
+
this.handlers = handlers;
|
|
62
|
+
this.maxWriteSize = maxWriteSize;
|
|
63
|
+
this.claims = claims;
|
|
64
|
+
}
|
|
65
|
+
/** Stale after a silent drop. The library's timeouts are what find a dead link. */
|
|
66
|
+
get isConnected() {
|
|
67
|
+
return !this.lost && !this.closed && this.peripheral?.isConnected === true;
|
|
68
|
+
}
|
|
69
|
+
/** Discover, then subscribe: the link is live only once this resolves. */
|
|
70
|
+
async attach(peripheral) {
|
|
71
|
+
this.peripheral = peripheral;
|
|
72
|
+
this.assertCurrent();
|
|
73
|
+
peripheral.once("disconnect", this.onDisconnect);
|
|
74
|
+
const characteristics = await (await peripheral.getService(require_shared.compactUuid(require_constants.SERVICE_UUID))).discoverCharacteristics();
|
|
75
|
+
const notify = find(characteristics, require_constants.VEHICLE_NOTIFY_UUID);
|
|
76
|
+
if (!notify || !find(characteristics, "00000212-b2d1-43f0-9b88-960cebf8b91e")) throw new Error("The vehicle's write or notify characteristic was not found.");
|
|
77
|
+
this.assertCurrent();
|
|
78
|
+
this.characteristics = characteristics;
|
|
79
|
+
await notify.subscribeToNotifications((data) => this.handlers.onNotify(data));
|
|
80
|
+
this.subscribed = true;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Homey hands every attempt the same link, and one subscription per
|
|
84
|
+
* characteristic. A superseded attempt stops here, before it changes either.
|
|
85
|
+
*/
|
|
86
|
+
assertCurrent() {
|
|
87
|
+
if (this.claims.superseded(this.id, this)) throw new Error("A newer connect attempt took over this link.");
|
|
88
|
+
}
|
|
89
|
+
async write(data) {
|
|
90
|
+
const characteristic = find(this.characteristics, require_constants.VEHICLE_WRITE_UUID);
|
|
91
|
+
if (!characteristic) throw new require_transport.BleNotSubmittedError("The vehicle write characteristic was not found.");
|
|
92
|
+
if (!this.isConnected) throw new require_transport.BleNotSubmittedError("The link is down.");
|
|
93
|
+
await characteristic.write(Buffer.from(data));
|
|
94
|
+
}
|
|
95
|
+
async read(characteristic) {
|
|
96
|
+
const target = characteristic === "version" ? find(this.characteristics, require_constants.VERSION_UUID) : find(await this.discoverGenericAccess(), characteristic === "deviceName" ? require_constants.NAME_UUID : require_constants.APPEARANCE_UUID);
|
|
97
|
+
if (!target) throw new require_transport.BleNotSubmittedError(`The ${characteristic} characteristic was not found.`);
|
|
98
|
+
return target.read();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Homey's stack may hide this service, which BLE-00 step 8 settles. A failed
|
|
102
|
+
* lookup leaves a characteristic unresolved on a healthy link, so it must not
|
|
103
|
+
* read as a lost one.
|
|
104
|
+
*/
|
|
105
|
+
discoverGenericAccess() {
|
|
106
|
+
const { peripheral } = this;
|
|
107
|
+
if (!peripheral) return Promise.reject(new require_transport.BleNotSubmittedError("The link is down."));
|
|
108
|
+
this.genericAccess ??= peripheral.getService(require_shared.compactUuid(require_constants.GENERIC_ACCESS_UUID)).then((service) => service.discoverCharacteristics()).catch((cause) => {
|
|
109
|
+
this.genericAccess = void 0;
|
|
110
|
+
throw new require_transport.BleNotSubmittedError("Generic Access could not be resolved.", { cause });
|
|
111
|
+
});
|
|
112
|
+
return this.genericAccess;
|
|
113
|
+
}
|
|
114
|
+
disconnect() {
|
|
115
|
+
this.closing ??= (async () => {
|
|
116
|
+
this.closed = true;
|
|
117
|
+
const { peripheral, claims, id } = this;
|
|
118
|
+
peripheral?.removeListener("disconnect", this.onDisconnect);
|
|
119
|
+
const superseded = claims.superseded(id, this);
|
|
120
|
+
claims.release(id, this);
|
|
121
|
+
if (superseded || !peripheral) return;
|
|
122
|
+
if (this.subscribed) await require_shared.quietly(() => require_async.withTimeout(() => find(this.characteristics, require_constants.VEHICLE_NOTIFY_UUID).unsubscribeFromNotifications(), HOMEY_UNSUBSCRIBE_TIMEOUT_MS));
|
|
123
|
+
await require_shared.quietly(() => peripheral.disconnect());
|
|
124
|
+
})();
|
|
125
|
+
return this.closing;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var HomeyBleTransport = class {
|
|
129
|
+
ble;
|
|
130
|
+
maxWriteSize;
|
|
131
|
+
claims = new require_shared.LinkClaims();
|
|
132
|
+
scanTimeoutMs = HOMEY_SCAN_TIMEOUT_MS;
|
|
133
|
+
/** @param ble `this.homey.ble`. The app needs the `homey:wireless:ble` permission. */
|
|
134
|
+
constructor(ble, options = {}) {
|
|
135
|
+
this.ble = ble;
|
|
136
|
+
this.maxWriteSize = options.maxWriteSize === void 0 ? null : options.maxWriteSize;
|
|
137
|
+
}
|
|
138
|
+
/** Takes Homey's fixed window, whatever `timeoutMs` says. Never pass `discover` a filter. */
|
|
139
|
+
async scan({ matches }) {
|
|
140
|
+
return (await this.ble.discover()).filter((advertisement) => !matches || advertisement.localName !== void 0 && matches(advertisement.localName)).map((advertisement) => ({
|
|
141
|
+
id: advertisement.uuid,
|
|
142
|
+
name: advertisement.localName,
|
|
143
|
+
rssi: advertisement.rssi,
|
|
144
|
+
connectable: advertisement.connectable
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
async connect(device, handlers) {
|
|
148
|
+
const connection = new HomeyConnection(device.id, handlers, this.maxWriteSize, this.claims);
|
|
149
|
+
this.claims.claim(device.id, connection);
|
|
150
|
+
try {
|
|
151
|
+
const peripheral = await (await this.ble.find(device.id)).connect();
|
|
152
|
+
await require_async.withTimeout(() => connection.attach(peripheral), HOMEY_SETUP_TIMEOUT_MS);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
await connection.disconnect();
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return connection;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
exports.HOMEY_MAX_WRITE_SIZE = HOMEY_MAX_WRITE_SIZE;
|
|
161
|
+
exports.HOMEY_SCAN_TIMEOUT_MS = HOMEY_SCAN_TIMEOUT_MS;
|
|
162
|
+
exports.HOMEY_SETUP_TIMEOUT_MS = HOMEY_SETUP_TIMEOUT_MS;
|
|
163
|
+
exports.HOMEY_UNSUBSCRIBE_TIMEOUT_MS = HOMEY_UNSUBSCRIBE_TIMEOUT_MS;
|
|
164
|
+
exports.HOMEY_VEHICLE_OPTIONS = HOMEY_VEHICLE_OPTIONS;
|
|
165
|
+
exports.HomeyBleTransport = HomeyBleTransport;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { BleConnection, BleDeviceRef, BleLinkHandlers, BleScanOptions, BleTransport } from "./transport.cjs";
|
|
2
|
+
/**
|
|
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.
|
|
8
|
+
*/
|
|
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;
|
|
16
|
+
/**
|
|
17
|
+
* For `connect_timeout_ms` and `max_connect_attempts`, not applied by this
|
|
18
|
+
* adapter: `discover()` caches for 30 s, so a fast retry sees old data, and
|
|
19
|
+
* these fit Homey's 9 s action window. BLE-00 step 11 sets both, against its
|
|
20
|
+
* target of a connect under 4 s in 9 of 10. A flow with no such window should
|
|
21
|
+
* keep the library's defaults until then, as a link slower than this never lands.
|
|
22
|
+
*/
|
|
23
|
+
export declare const HOMEY_VEHICLE_OPTIONS: {
|
|
24
|
+
readonly connect_timeout_ms: 4000;
|
|
25
|
+
readonly max_connect_attempts: 1;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Bound on discovery and subscribing once a link is up. Homey has no timeout of
|
|
29
|
+
* its own, and if these hang, nothing else could release the vehicle's slot.
|
|
30
|
+
* BLE-00 steps 3 and 4 show what a slow link takes.
|
|
31
|
+
*/
|
|
32
|
+
export declare const HOMEY_SETUP_TIMEOUT_MS = 20000;
|
|
33
|
+
/** Bound on unsubscribing, which must not keep `disconnect` from running on a dead link. BLE-00 step 10. */
|
|
34
|
+
export declare const HOMEY_UNSUBSCRIBE_TIMEOUT_MS = 1000;
|
|
35
|
+
export interface HomeyCharacteristicLike {
|
|
36
|
+
uuid: string;
|
|
37
|
+
read(): Promise<Buffer>;
|
|
38
|
+
/** Always a write with response: the only kind Homey offers. */
|
|
39
|
+
write(data: Buffer): Promise<unknown>;
|
|
40
|
+
subscribeToNotifications(callback: (data: Buffer) => void): Promise<void>;
|
|
41
|
+
unsubscribeFromNotifications(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
export interface HomeyServiceLike {
|
|
44
|
+
discoverCharacteristics(): Promise<HomeyCharacteristicLike[]>;
|
|
45
|
+
}
|
|
46
|
+
export interface HomeyPeripheralLike {
|
|
47
|
+
/** Can stay true after a silent drop, so it is only ever a faster signal. BLE-00 step 9 measures it. */
|
|
48
|
+
readonly isConnected: boolean;
|
|
49
|
+
getService(uuid: string): Promise<HomeyServiceLike>;
|
|
50
|
+
disconnect(): Promise<void>;
|
|
51
|
+
/** Not guaranteed on every drop, but if it fires, the link is gone. BLE-00 step 9 measures it. */
|
|
52
|
+
once(event: "disconnect", listener: () => void): unknown;
|
|
53
|
+
removeListener(event: "disconnect", listener: () => void): unknown;
|
|
54
|
+
}
|
|
55
|
+
export interface HomeyAdvertisementLike {
|
|
56
|
+
uuid: string;
|
|
57
|
+
/** Typed as a string, but Homey's guide says it is optional. BLE-00 step 2 measures how often it is there. */
|
|
58
|
+
localName?: string;
|
|
59
|
+
rssi?: number;
|
|
60
|
+
connectable?: boolean;
|
|
61
|
+
/** If already connected, returns the existing connection. It has no timeout: the library bounds it. */
|
|
62
|
+
connect(): Promise<HomeyPeripheralLike>;
|
|
63
|
+
}
|
|
64
|
+
/** What this adapter needs of `this.homey.ble`. */
|
|
65
|
+
export interface HomeyBleManagerLike {
|
|
66
|
+
/** About 10 s, fixed, when not cached (BLE-00 HW-0). Called with no filter, because the vehicle advertises no service. */
|
|
67
|
+
discover(): Promise<HomeyAdvertisementLike[]>;
|
|
68
|
+
find(uuid: string): Promise<HomeyAdvertisementLike>;
|
|
69
|
+
}
|
|
70
|
+
export interface HomeyBleTransportOptions {
|
|
71
|
+
/** Bytes per write, or `null` for a whole frame in one write. Defaults to `HOMEY_MAX_WRITE_SIZE`. */
|
|
72
|
+
maxWriteSize?: number | null;
|
|
73
|
+
}
|
|
74
|
+
export declare class HomeyBleTransport implements BleTransport {
|
|
75
|
+
private readonly ble;
|
|
76
|
+
private readonly maxWriteSize;
|
|
77
|
+
private readonly claims;
|
|
78
|
+
readonly scanTimeoutMs = 15000;
|
|
79
|
+
/** @param ble `this.homey.ble`. The app needs the `homey:wireless:ble` permission. */
|
|
80
|
+
constructor(ble: HomeyBleManagerLike, options?: HomeyBleTransportOptions);
|
|
81
|
+
/** Takes Homey's fixed window, whatever `timeoutMs` says. Never pass `discover` a filter. */
|
|
82
|
+
scan({ matches }: BleScanOptions): Promise<BleDeviceRef[]>;
|
|
83
|
+
connect(device: BleDeviceRef, handlers: BleLinkHandlers): Promise<BleConnection>;
|
|
84
|
+
}
|