tesla-fleet-api 0.3.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 +32 -0
- package/dist/_virtual/_rolldown/runtime.mjs +12 -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/charging.cjs +53 -0
- package/dist/charging.d.cts +31 -0
- package/dist/charging.d.mts +30 -0
- package/dist/charging.mjs +53 -0
- package/dist/commands.cjs +1797 -0
- package/dist/commands.d.cts +881 -0
- package/dist/commands.d.mts +881 -0
- package/dist/commands.mjs +1791 -0
- package/dist/const.cjs +322 -0
- package/dist/const.d.cts +308 -0
- package/dist/const.d.mts +308 -0
- package/dist/const.mjs +282 -0
- package/dist/energy.cjs +142 -0
- package/dist/energy.d.cts +101 -0
- package/dist/energy.d.mts +100 -0
- package/dist/energy.mjs +142 -0
- 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 +457 -0
- package/dist/energyspecific.d.cts +258 -0
- package/dist/energyspecific.d.mts +258 -0
- package/dist/energyspecific.mjs +451 -0
- package/dist/exceptions.cjs +1488 -0
- package/dist/exceptions.d.cts +1143 -0
- package/dist/exceptions.d.mts +1143 -0
- package/dist/exceptions.mjs +1325 -0
- 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 +287 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +27 -0
- 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/partner.cjs +32 -0
- package/dist/partner.d.cts +24 -0
- package/dist/partner.d.mts +23 -0
- package/dist/partner.mjs +32 -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 -0
- package/dist/router.d.mts +3 -0
- package/dist/router.mjs +3 -0
- package/dist/signing/crypto.cjs +59 -0
- package/dist/signing/crypto.mjs +52 -0
- 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 +186 -0
- package/dist/signing/session.d.cts +106 -0
- package/dist/signing/session.d.mts +106 -0
- package/dist/signing/session.mjs +184 -0
- package/dist/tariff.cjs +319 -0
- package/dist/tariff.d.cts +40 -0
- package/dist/tariff.d.mts +40 -0
- package/dist/tariff.mjs +317 -0
- package/dist/teslafleetapi.cjs +326 -0
- package/dist/teslafleetapi.d.cts +160 -0
- package/dist/teslafleetapi.d.mts +160 -0
- package/dist/teslafleetapi.mjs +326 -0
- package/dist/teslemetry.cjs +183 -0
- package/dist/teslemetry.d.cts +113 -0
- package/dist/teslemetry.d.mts +113 -0
- package/dist/teslemetry.mjs +181 -0
- 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 +71 -0
- package/dist/tessie.d.cts +69 -0
- package/dist/tessie.d.mts +69 -0
- package/dist/tessie.mjs +71 -0
- 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/commands.d.cts +28 -0
- package/dist/types/commands.d.mts +28 -0
- package/dist/types/live_status.d.cts +30 -0
- package/dist/types/live_status.d.mts +30 -0
- package/dist/types/products.d.cts +58 -0
- package/dist/types/products.d.mts +58 -0
- package/dist/types/responses.d.cts +13 -0
- package/dist/types/responses.d.mts +13 -0
- package/dist/types/site_info.d.cts +154 -0
- package/dist/types/site_info.d.mts +154 -0
- package/dist/types/vehicle.d.cts +33 -0
- package/dist/types/vehicle.d.mts +33 -0
- package/dist/types/vehicle_data.d.cts +278 -0
- package/dist/types/vehicle_data.d.mts +278 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +9 -0
- package/dist/types.d.mts +9 -0
- package/dist/types.mjs +1 -0
- package/dist/user.cjs +28 -0
- package/dist/user.d.cts +17 -0
- package/dist/user.d.mts +16 -0
- package/dist/user.mjs +28 -0
- 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 +847 -0
- package/dist/vehicle.d.cts +566 -0
- package/dist/vehicle.d.mts +565 -0
- package/dist/vehicle.mjs +847 -0
- package/dist/vehiclesigned.cjs +35 -0
- package/dist/vehiclesigned.d.cts +23 -0
- package/dist/vehiclesigned.d.mts +22 -0
- package/dist/vehiclesigned.mjs +35 -0
- package/dist/vehiclespecific.cjs +715 -0
- package/dist/vehiclespecific.d.cts +497 -0
- package/dist/vehiclespecific.d.mts +496 -0
- package/dist/vehiclespecific.mjs +715 -0
- package/package.json +178 -38
- package/dist/charging.d.ts +0 -30
- package/dist/charging.js +0 -51
- package/dist/charging.js.map +0 -1
- package/dist/commands.d.ts +0 -352
- package/dist/commands.js +0 -875
- package/dist/commands.js.map +0 -1
- package/dist/energy.d.ts +0 -94
- package/dist/energy.js +0 -125
- package/dist/energy.js.map +0 -1
- package/dist/energyspecific.d.ts +0 -77
- package/dist/energyspecific.js +0 -97
- package/dist/energyspecific.js.map +0 -1
- package/dist/helpers.d.ts +0 -2
- package/dist/helpers.js +0 -10
- package/dist/helpers.js.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -15
- package/dist/index.js.map +0 -1
- package/dist/partner.d.ts +0 -23
- package/dist/partner.js +0 -31
- package/dist/partner.js.map +0 -1
- package/dist/signing/crypto.d.ts +0 -10
- package/dist/signing/crypto.js +0 -26
- package/dist/signing/crypto.js.map +0 -1
- package/dist/signing/errors.d.ts +0 -29
- package/dist/signing/errors.js +0 -40
- package/dist/signing/errors.js.map +0 -1
- package/dist/signing/session.d.ts +0 -69
- package/dist/signing/session.js +0 -102
- package/dist/signing/session.js.map +0 -1
- package/dist/tariff.d.ts +0 -29
- package/dist/tariff.js +0 -302
- package/dist/tariff.js.map +0 -1
- package/dist/teslafleetapi.d.ts +0 -58
- package/dist/teslafleetapi.js +0 -120
- package/dist/teslafleetapi.js.map +0 -1
- package/dist/teslemetry.d.ts +0 -29
- package/dist/teslemetry.js +0 -39
- package/dist/teslemetry.js.map +0 -1
- package/dist/tessie.d.ts +0 -23
- package/dist/tessie.js +0 -28
- package/dist/tessie.js.map +0 -1
- package/dist/types/commands.d.ts +0 -44
- package/dist/types/commands.js +0 -11
- package/dist/types/commands.js.map +0 -1
- package/dist/types/live_status.d.ts +0 -26
- package/dist/types/live_status.js +0 -2
- package/dist/types/live_status.js.map +0 -1
- package/dist/types/products.d.ts +0 -54
- package/dist/types/products.js +0 -2
- package/dist/types/products.js.map +0 -1
- package/dist/types/responses.d.ts +0 -5
- package/dist/types/responses.js +0 -2
- package/dist/types/responses.js.map +0 -1
- package/dist/types/site_info.d.ts +0 -150
- package/dist/types/site_info.js +0 -2
- package/dist/types/site_info.js.map +0 -1
- package/dist/types/vehicle.d.ts +0 -23
- package/dist/types/vehicle.js +0 -10
- package/dist/types/vehicle.js.map +0 -1
- package/dist/types/vehicle_data.d.ts +0 -274
- package/dist/types/vehicle_data.js +0 -2
- package/dist/types/vehicle_data.js.map +0 -1
- package/dist/user.d.ts +0 -16
- package/dist/user.js +0 -26
- package/dist/user.js.map +0 -1
- package/dist/vehicle.d.ts +0 -548
- package/dist/vehicle.js +0 -774
- package/dist/vehicle.js.map +0 -1
- package/dist/vehiclesigned.d.ts +0 -13
- package/dist/vehiclesigned.js +0 -19
- package/dist/vehiclesigned.js.map +0 -1
- package/dist/vehiclespecific.d.ts +0 -432
- package/dist/vehiclespecific.js +0 -601
- package/dist/vehiclespecific.js.map +0 -1
- package/src/charging.ts +0 -66
- package/src/commands.ts +0 -1134
- package/src/energy.ts +0 -217
- package/src/energyspecific.ts +0 -116
- package/src/helpers.ts +0 -11
- package/src/index.ts +0 -21
- package/src/partner.ts +0 -37
- package/src/signing/crypto.ts +0 -29
- package/src/signing/errors.ts +0 -50
- package/src/signing/session.ts +0 -132
- package/src/tariff.ts +0 -375
- package/src/teslafleetapi.ts +0 -182
- package/src/teslemetry.ts +0 -54
- package/src/tessie.ts +0 -42
- package/src/types/commands.ts +0 -62
- package/src/types/live_status.ts +0 -27
- package/src/types/products.ts +0 -61
- package/src/types/responses.ts +0 -1
- package/src/types/site_info.ts +0 -159
- package/src/types/vehicle.ts +0 -33
- package/src/types/vehicle_data.ts +0 -286
- package/src/user.ts +0 -35
- package/src/vehicle.ts +0 -886
- package/src/vehiclesigned.ts +0 -20
- package/src/vehiclespecific.ts +0 -818
|
@@ -0,0 +1,1272 @@
|
|
|
1
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_const = require("../const.cjs");
|
|
3
|
+
const require_commands = require("../commands.cjs");
|
|
4
|
+
const require_log = require("../log.cjs");
|
|
5
|
+
const require_exceptions = require("../exceptions.cjs");
|
|
6
|
+
const require_async = require("./async.cjs");
|
|
7
|
+
const require_broadcast = require("./broadcast.cjs");
|
|
8
|
+
const require_constants = require("./constants.cjs");
|
|
9
|
+
const require_framing = require("./framing.cjs");
|
|
10
|
+
const require_names = require("./names.cjs");
|
|
11
|
+
const require_transport = require("./transport.cjs");
|
|
12
|
+
const require_verify = require("./verify.cjs");
|
|
13
|
+
let crypto = require("crypto");
|
|
14
|
+
crypto = require_runtime.__toESM(crypto, 1);
|
|
15
|
+
let _teslemetry_tesla_protocol_command_universal_message = require("@teslemetry/tesla-protocol/command/universal_message");
|
|
16
|
+
let _teslemetry_tesla_protocol_command_vcsec = require("@teslemetry/tesla-protocol/command/vcsec");
|
|
17
|
+
let _teslemetry_tesla_protocol_command_car_server = require("@teslemetry/tesla-protocol/command/car_server");
|
|
18
|
+
let _teslemetry_tesla_protocol_command_keys = require("@teslemetry/tesla-protocol/command/keys");
|
|
19
|
+
// src/bluetooth/vehiclebluetooth.ts
|
|
20
|
+
const VCSEC = _teslemetry_tesla_protocol_command_universal_message.Domain.DOMAIN_VEHICLE_SECURITY;
|
|
21
|
+
const INFOTAINMENT = _teslemetry_tesla_protocol_command_universal_message.Domain.DOMAIN_INFOTAINMENT;
|
|
22
|
+
/** What Device Name starts with once the vehicle's name is available. */
|
|
23
|
+
const DISPLAY_NAME_PREFIX = "🔑 ";
|
|
24
|
+
const DISPLAY_NAME_RETRY_MS = 1e3;
|
|
25
|
+
function success() {
|
|
26
|
+
return { response: {
|
|
27
|
+
result: true,
|
|
28
|
+
reason: ""
|
|
29
|
+
} };
|
|
30
|
+
}
|
|
31
|
+
/** One GATT connection, or an attempt at one, with everything that must not outlive it. */
|
|
32
|
+
var Link = class {
|
|
33
|
+
connection;
|
|
34
|
+
/** Holds one adapter call at a time, so frames and reads never interleave. */
|
|
35
|
+
gatt = new require_async.AsyncLock();
|
|
36
|
+
/** Per link, so a partial frame from a dead link can never join the next link's. */
|
|
37
|
+
buffer;
|
|
38
|
+
/** Aborts when the link is lost, ending every wait on it. */
|
|
39
|
+
closed = new AbortController();
|
|
40
|
+
constructor(onMessage) {
|
|
41
|
+
this.buffer = new require_framing.ReassemblingBuffer(onMessage);
|
|
42
|
+
}
|
|
43
|
+
get lost() {
|
|
44
|
+
return this.closed.signal.aborted;
|
|
45
|
+
}
|
|
46
|
+
close() {
|
|
47
|
+
this.closed.abort(/* @__PURE__ */ new Error("The Bluetooth link was lost."));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A vehicle reached directly over Bluetooth, with commands signed by AES-GCM.
|
|
52
|
+
*
|
|
53
|
+
* All Tesla protocol logic lives here and in `Commands`. A `BleTransport`
|
|
54
|
+
* supplies raw GATT only, so the same class runs on any backend, Homey's
|
|
55
|
+
* included. Backends may hang and may misreport their link, so every backend
|
|
56
|
+
* call is bounded, and a failed or timed-out operation is what counts as a dead
|
|
57
|
+
* link.
|
|
58
|
+
*
|
|
59
|
+
* `Commands.dispatch` holds a VCSEC session's lock across `_send`, and that lock
|
|
60
|
+
* is not reentrant, so `_send` serializes on its own `domainSendLock` and must
|
|
61
|
+
* never take `Session.withLock`.
|
|
62
|
+
*
|
|
63
|
+
* Sessions survive a reconnect, and the vehicle key they pinned at first contact
|
|
64
|
+
* survives with them: a new link may re-handshake, but nothing here clears a
|
|
65
|
+
* session or lets a new link trust a new key.
|
|
66
|
+
*
|
|
67
|
+
* A mutating command that times out is unresolved, not failed: in field data 2
|
|
68
|
+
* of 3 failed writes had executed anyway. So a timed-out command is never sent
|
|
69
|
+
* again. The outcomes a caller can see:
|
|
70
|
+
*
|
|
71
|
+
* - `BluetoothTransportError`: provably never reached the radio. Safe to replay.
|
|
72
|
+
* - `BluetoothCommandFailed`: state proved it did not apply. Safe to replay.
|
|
73
|
+
* - `BluetoothUnconfirmedCommand`: it may have run. Never replay it.
|
|
74
|
+
* - A plain `BluetoothTimeout`: a read, or a step before any write.
|
|
75
|
+
* - Best-effort success, when `raise_unconfirmed` is off and nothing resolved it.
|
|
76
|
+
* `charge_port_door_open` under `"verify"` is the exception, and raises.
|
|
77
|
+
*/
|
|
78
|
+
var VehicleBluetooth = class extends require_broadcast.BroadcastListeners {
|
|
79
|
+
_transport_name = "bluetooth";
|
|
80
|
+
authMethod = "aes";
|
|
81
|
+
/** Reply wait with data. */
|
|
82
|
+
defaultTimeoutMs = 5e3;
|
|
83
|
+
/** Reply wait for a VCSEC actuation. A lost ack is inconclusive, so a shorter wait loses nothing. */
|
|
84
|
+
actuationTimeoutMs = 2e3;
|
|
85
|
+
/** Wait for data after a bare ack. */
|
|
86
|
+
ackFollowupTimeoutMs = 2e3;
|
|
87
|
+
/** How long a command woken by `wake_if_asleep` waits for infotainment. */
|
|
88
|
+
wakeTimeoutMs = require_constants.DEFAULT_WAKE_TIMEOUT_MS;
|
|
89
|
+
/** How often a wake is resent while infotainment stays silent. */
|
|
90
|
+
wakeResendIntervalMs = require_constants.WAKE_RESEND_INTERVAL_MS;
|
|
91
|
+
/** How long an awake car's infotainment may take to finish booting. */
|
|
92
|
+
infotainmentBootTimeoutMs = require_constants.INFOTAINMENT_BOOT_TIMEOUT_MS;
|
|
93
|
+
/** How long a charge-port opening has to show in charge state. */
|
|
94
|
+
chargePortVerifyTimeoutMs = require_constants.CHARGE_PORT_VERIFY_TIMEOUT_MS;
|
|
95
|
+
/** Between charge-state polls. */
|
|
96
|
+
chargePortVerifyIntervalMs = 250;
|
|
97
|
+
/** Bound on one keepalive read. */
|
|
98
|
+
keepaliveTimeoutMs = 2e3;
|
|
99
|
+
/** The vehicle's BLE name, with the usual `C` suffix. */
|
|
100
|
+
ble_name;
|
|
101
|
+
confirmation;
|
|
102
|
+
raise_unconfirmed;
|
|
103
|
+
wake_if_asleep;
|
|
104
|
+
keepalive_interval_ms;
|
|
105
|
+
transport;
|
|
106
|
+
queues = {
|
|
107
|
+
[VCSEC]: new require_async.ReplyQueue(),
|
|
108
|
+
[INFOTAINMENT]: new require_async.ReplyQueue()
|
|
109
|
+
};
|
|
110
|
+
maxConnectAttempts;
|
|
111
|
+
connectTimeoutMs;
|
|
112
|
+
scanTimeoutMs;
|
|
113
|
+
wakeProbeTimeoutMs;
|
|
114
|
+
domainSendLock = {
|
|
115
|
+
[VCSEC]: new require_async.AsyncLock(),
|
|
116
|
+
[INFOTAINMENT]: new require_async.AsyncLock()
|
|
117
|
+
};
|
|
118
|
+
connectLock = new require_async.AsyncLock();
|
|
119
|
+
broadcastWatchers = {};
|
|
120
|
+
device;
|
|
121
|
+
link = null;
|
|
122
|
+
/** Closing a lost link. A new connect waits for it: a late disconnect could end the new link. */
|
|
123
|
+
teardown = Promise.resolve();
|
|
124
|
+
keepaliveTimer = null;
|
|
125
|
+
/** When GATT traffic last moved, on the monotonic clock. */
|
|
126
|
+
lastActivity = 0;
|
|
127
|
+
connected = false;
|
|
128
|
+
connectionListeners = new require_broadcast.ListenerRegistry((message) => this.log(message));
|
|
129
|
+
constructor(parent, vin, options) {
|
|
130
|
+
super(parent, vin, options.privateKey);
|
|
131
|
+
const confirmation = options.confirmation ?? "ack";
|
|
132
|
+
if (![
|
|
133
|
+
"optimistic",
|
|
134
|
+
"ack",
|
|
135
|
+
"verify"
|
|
136
|
+
].includes(confirmation)) throw new Error("confirmation must be one of \"optimistic\", \"ack\" or \"verify\".");
|
|
137
|
+
this.confirmation = confirmation;
|
|
138
|
+
this.raise_unconfirmed = options.raise_unconfirmed ?? false;
|
|
139
|
+
this.transport = options.transport;
|
|
140
|
+
this.device = options.device;
|
|
141
|
+
this.keepalive_interval_ms = options.keepalive_interval_ms === void 0 ? require_constants.DEFAULT_KEEPALIVE_INTERVAL_MS : options.keepalive_interval_ms;
|
|
142
|
+
this.wake_if_asleep = options.wake_if_asleep ?? true;
|
|
143
|
+
this.maxConnectAttempts = options.max_connect_attempts ?? 3;
|
|
144
|
+
this.connectTimeoutMs = options.connect_timeout_ms ?? 2e4;
|
|
145
|
+
this.scanTimeoutMs = options.scan_timeout_ms ?? 1e4;
|
|
146
|
+
this.wakeProbeTimeoutMs = options.wake_probe_timeout_ms ?? 1e3;
|
|
147
|
+
if (this.wakeProbeTimeoutMs <= 0) throw new RangeError("wake_probe_timeout_ms must be greater than 0.");
|
|
148
|
+
this.ble_name = require_names.get_name(vin);
|
|
149
|
+
}
|
|
150
|
+
/** Whether `confirmation` is `"optimistic"`. */
|
|
151
|
+
get optimistic() {
|
|
152
|
+
return this.confirmation === "optimistic";
|
|
153
|
+
}
|
|
154
|
+
/** Whether `confirmation` is `"verify"`. */
|
|
155
|
+
get verify_commands() {
|
|
156
|
+
return this.confirmation === "verify";
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Find this vehicle. Never filters by service, because the vehicle advertises
|
|
160
|
+
* none. A device with free connection slots wins, then the strongest signal.
|
|
161
|
+
* @throws BluetoothTransportError when the scan fails or finds nothing.
|
|
162
|
+
*/
|
|
163
|
+
async find_vehicle({ name, address, timeoutMs = this.scanTimeoutMs } = {}) {
|
|
164
|
+
const matchesName = (seen) => name !== void 0 ? seen === name : require_names.name_matches_vin(seen, this.vin);
|
|
165
|
+
const accepts = (device) => address !== void 0 ? device.id === address : device.name !== void 0 && matchesName(device.name);
|
|
166
|
+
let seen;
|
|
167
|
+
try {
|
|
168
|
+
seen = await require_async.withTimeout(() => this.transport.scan({
|
|
169
|
+
timeoutMs,
|
|
170
|
+
matches: address === void 0 ? matchesName : void 0
|
|
171
|
+
}), timeoutMs);
|
|
172
|
+
} catch (cause) {
|
|
173
|
+
throw new require_exceptions.BluetoothTransportError(null, null, { cause });
|
|
174
|
+
}
|
|
175
|
+
const [device] = seen.filter(accepts).sort((a, b) => Number(a.connectable === false) - Number(b.connectable === false) || (b.rssi ?? -Infinity) - (a.rssi ?? -Infinity));
|
|
176
|
+
if (!device) throw new require_exceptions.BluetoothTransportError("Vehicle not found in the scan.");
|
|
177
|
+
this.device = device;
|
|
178
|
+
return device;
|
|
179
|
+
}
|
|
180
|
+
/** Assign the device later connections use. */
|
|
181
|
+
set_device(device) {
|
|
182
|
+
this.device = device;
|
|
183
|
+
}
|
|
184
|
+
/** The device later connections use, if one has been found or set. */
|
|
185
|
+
get_device() {
|
|
186
|
+
return this.device;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Connect, unless a link is up. Scans first if no device is known. Python
|
|
190
|
+
* raises there instead, but a Homey app has no scan of its own to run.
|
|
191
|
+
* @throws BluetoothTransportError when every attempt fails, with the last failure as `cause`.
|
|
192
|
+
*/
|
|
193
|
+
async connect(maxAttempts = this.maxConnectAttempts) {
|
|
194
|
+
await this.connectLock.run(async () => {
|
|
195
|
+
const current = this.link;
|
|
196
|
+
if (current?.connection.isConnected) return;
|
|
197
|
+
if (current) this.loseLink(current, "backend reports it down");
|
|
198
|
+
await this.teardown;
|
|
199
|
+
const device = this.device ?? await this.find_vehicle();
|
|
200
|
+
let cause;
|
|
201
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
202
|
+
await this.teardown;
|
|
203
|
+
await this.connectOnce(device);
|
|
204
|
+
return;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
this.log(`BLE connect attempt ${attempt}/${maxAttempts} failed: ${describe(error)}`);
|
|
207
|
+
cause = error;
|
|
208
|
+
}
|
|
209
|
+
throw new require_exceptions.BluetoothTransportError(null, null, { cause });
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Connect if the link is down, as every send does. */
|
|
213
|
+
async connect_if_needed(maxAttempts = this.maxConnectAttempts) {
|
|
214
|
+
await this.connect(maxAttempts);
|
|
215
|
+
}
|
|
216
|
+
async connectOnce(device) {
|
|
217
|
+
const link = new Link((message) => this.onMessage(message));
|
|
218
|
+
const connecting = (async () => this.transport.connect(device, {
|
|
219
|
+
onNotify: (chunk) => this.onNotify(link, chunk),
|
|
220
|
+
onDisconnect: () => this.loseLink(link, "backend reported a disconnect")
|
|
221
|
+
}))();
|
|
222
|
+
let connection;
|
|
223
|
+
try {
|
|
224
|
+
connection = await require_async.withTimeout(() => connecting, this.connectTimeoutMs);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
link.close();
|
|
227
|
+
if (error instanceof require_async.BleCallTimeout) {
|
|
228
|
+
const closing = connecting.then((late) => this.closeConnection(late), () => void 0);
|
|
229
|
+
this.teardown = Promise.all([this.teardown, require_async.withTimeout(() => closing, require_constants.LINK_TEARDOWN_TIMEOUT_MS).catch(() => void 0)]);
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
if (link.lost || !connection.isConnected) {
|
|
234
|
+
link.close();
|
|
235
|
+
await this.closeConnection(connection);
|
|
236
|
+
throw new Error("The link dropped while connecting.");
|
|
237
|
+
}
|
|
238
|
+
link.connection = connection;
|
|
239
|
+
this.link = link;
|
|
240
|
+
this.startKeepalive();
|
|
241
|
+
this.setConnected(true);
|
|
242
|
+
}
|
|
243
|
+
/** Close a connection, bounded and never throwing: the failure behind it is what gets reported. */
|
|
244
|
+
async closeConnection(connection) {
|
|
245
|
+
try {
|
|
246
|
+
await require_async.withTimeout(() => connection.disconnect(), require_constants.LINK_TEARDOWN_TIMEOUT_MS);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
this.log(`BLE link teardown failed: ${describe(error)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Mark a link lost and start closing it. Only the current link has state to
|
|
253
|
+
* clear, so a late event from an old link or an abandoned attempt ends there.
|
|
254
|
+
*/
|
|
255
|
+
loseLink(link, reason) {
|
|
256
|
+
link.close();
|
|
257
|
+
if (this.link !== link) return;
|
|
258
|
+
this.link = null;
|
|
259
|
+
this.log(`BLE link lost: ${reason}`);
|
|
260
|
+
this.stopKeepalive();
|
|
261
|
+
this.teardown = Promise.all([this.teardown, this.closeConnection(link.connection)]);
|
|
262
|
+
this.setConnected(false);
|
|
263
|
+
}
|
|
264
|
+
/** Tear down the link and wait for it. The next send reconnects. */
|
|
265
|
+
async dropLink(reason) {
|
|
266
|
+
if (this.link) this.loseLink(this.link, reason);
|
|
267
|
+
await this.teardown;
|
|
268
|
+
}
|
|
269
|
+
/** Drop the link after a failed session setup, unless the failure never touched it. */
|
|
270
|
+
async dropLinkOnFailure(reason, error) {
|
|
271
|
+
if (!(error instanceof require_exceptions.SigningDisabled)) await this.dropLink(`${reason}: ${describe(error)}`);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Disconnect, waiting out any connect in progress.
|
|
275
|
+
* @returns Whether there was a connection.
|
|
276
|
+
*/
|
|
277
|
+
async disconnect() {
|
|
278
|
+
return this.connectLock.run(async () => {
|
|
279
|
+
const connected = this.link !== null;
|
|
280
|
+
await this.dropLink("disconnect");
|
|
281
|
+
return connected;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Listen for the link coming up and going down. `true` fires once
|
|
286
|
+
* notifications are live, and `false` once the link is lost, for any reason
|
|
287
|
+
* and however it was noticed. Only real changes fire. A backend that drops
|
|
288
|
+
* silently makes `false` as late as the next operation to time out.
|
|
289
|
+
*/
|
|
290
|
+
listen_connection_status(callback) {
|
|
291
|
+
return this.connectionListeners.add(callback);
|
|
292
|
+
}
|
|
293
|
+
setConnected(connected) {
|
|
294
|
+
if (this.connected !== connected) {
|
|
295
|
+
this.connected = connected;
|
|
296
|
+
this.connectionListeners.emit(connected);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
startKeepalive() {
|
|
300
|
+
this.stopKeepalive();
|
|
301
|
+
if (this.keepalive_interval_ms) {
|
|
302
|
+
this.lastActivity = performance.now();
|
|
303
|
+
this.scheduleKeepalive(this.keepalive_interval_ms);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
stopKeepalive() {
|
|
307
|
+
if (this.keepaliveTimer !== null) {
|
|
308
|
+
clearTimeout(this.keepaliveTimer);
|
|
309
|
+
this.keepaliveTimer = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
scheduleKeepalive(intervalMs) {
|
|
313
|
+
const idleMs = performance.now() - this.lastActivity;
|
|
314
|
+
this.keepaliveTimer = setTimeout(() => void this.keepaliveTick(intervalMs), Math.max(0, intervalMs - idleMs));
|
|
315
|
+
this.keepaliveTimer.unref?.();
|
|
316
|
+
}
|
|
317
|
+
/** Idle-triggered, so an active link pays nothing. It never raises, never reconnects and never wakes the car. */
|
|
318
|
+
async keepaliveTick(intervalMs) {
|
|
319
|
+
this.keepaliveTimer = null;
|
|
320
|
+
const link = this.link;
|
|
321
|
+
if (!link) return;
|
|
322
|
+
if (performance.now() - this.lastActivity >= intervalMs) {
|
|
323
|
+
await this.keepaliveRead(link);
|
|
324
|
+
this.lastActivity = performance.now();
|
|
325
|
+
}
|
|
326
|
+
if (this.link === link) this.scheduleKeepalive(intervalMs);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* One bounded read, with every failure swallowed. A read that times out
|
|
330
|
+
* still marks the link lost, as any read does here: on a backend that
|
|
331
|
+
* misreports its link, that is how a dead idle link is found before the next
|
|
332
|
+
* command writes to it. Python only drops the read.
|
|
333
|
+
*/
|
|
334
|
+
async keepaliveRead(link) {
|
|
335
|
+
if (!link.connection.isConnected) {
|
|
336
|
+
this.loseLink(link, "backend reports it down");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
await this.gattRead("version", this.keepaliveTimeoutMs);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
this.log(`BLE keepalive read failed: ${describe(error)}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** A backend calls this, so it must never throw. */
|
|
346
|
+
onNotify(link, chunk) {
|
|
347
|
+
if (link.lost) return;
|
|
348
|
+
this.lastActivity = performance.now();
|
|
349
|
+
try {
|
|
350
|
+
link.buffer.receive(chunk);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
this.log(`BLE notification failed: ${describe(error)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/** Route one whole message: replies to their domain's queue, the rest to whoever listens. */
|
|
356
|
+
onMessage(message) {
|
|
357
|
+
const address = message.toDestination?.routingAddress;
|
|
358
|
+
const domain = message.fromDestination?.domain ?? _teslemetry_tesla_protocol_command_universal_message.Domain.DOMAIN_BROADCAST;
|
|
359
|
+
if (!address || !this.fromDestination.equals(address)) {
|
|
360
|
+
this.broadcastWatchers[domain]?.onBroadcast(message);
|
|
361
|
+
this.dispatchBroadcast(domain, message);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const queue = this.queues[domain];
|
|
365
|
+
if (!queue) {
|
|
366
|
+
this.log(`BLE ignored a reply from unhandled domain ${domain}`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
queue.push(message);
|
|
370
|
+
}
|
|
371
|
+
/** Write one frame, with its chunks together, each bounded. */
|
|
372
|
+
writeFrame(link, frame, signal) {
|
|
373
|
+
const { connection } = link;
|
|
374
|
+
const maxWriteSize = connection.maxWriteSize === void 0 ? 20 : connection.maxWriteSize;
|
|
375
|
+
return link.gatt.run(async () => {
|
|
376
|
+
signal?.throwIfAborted();
|
|
377
|
+
let written = 0;
|
|
378
|
+
for (const chunk of require_framing.splitFrame(frame, maxWriteSize)) {
|
|
379
|
+
if (link.lost) throw new require_transport.BleNotSubmittedError("The link was lost before the frame was fully written.");
|
|
380
|
+
try {
|
|
381
|
+
await require_async.withTimeout(() => connection.write(chunk), require_constants.GATT_WRITE_TIMEOUT_MS, link.closed.signal);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (error instanceof require_transport.BleNotSubmittedError && written > 0) this.loseLink(link, "a frame stopped part way");
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
written++;
|
|
387
|
+
this.lastActivity = performance.now();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Read a characteristic, bounded. A rejected or timed-out read marks the link
|
|
393
|
+
* lost, whatever `isConnected` says, as that is how a dead link is found on a
|
|
394
|
+
* backend that misreports it. The exception is `BleNotSubmittedError` with the
|
|
395
|
+
* link reported up: no radio I/O happened, and a stack that hides Generic
|
|
396
|
+
* Access rejects those reads that way on a healthy link.
|
|
397
|
+
* @throws BluetoothTransportError when there is no link.
|
|
398
|
+
*/
|
|
399
|
+
async gattRead(characteristic, timeoutMs = require_constants.GATT_READ_TIMEOUT_MS) {
|
|
400
|
+
const link = this.link;
|
|
401
|
+
if (!link) throw new require_exceptions.BluetoothTransportError();
|
|
402
|
+
const { connection } = link;
|
|
403
|
+
return link.gatt.run(async () => {
|
|
404
|
+
if (link.lost) throw new require_exceptions.BluetoothTransportError();
|
|
405
|
+
try {
|
|
406
|
+
return await require_async.withTimeout(() => connection.read(characteristic), timeoutMs, link.closed.signal);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (!(error instanceof require_transport.BleNotSubmittedError) || !connection.isConnected) this.loseLink(link, `read failed: ${describe(error)}`);
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
async _send(msg, requires, options = {}) {
|
|
414
|
+
const { expectsData = true, optimistic = false, confirmBroadcast, onSubmitted, signal } = options;
|
|
415
|
+
const timeoutMs = options.timeoutMs ?? (expectsData ? this.defaultTimeoutMs : this.actuationTimeoutMs);
|
|
416
|
+
const domain = msg.toDestination?.domain;
|
|
417
|
+
const lock = domain === void 0 ? void 0 : this.domainSendLock[domain];
|
|
418
|
+
const queue = domain === void 0 ? void 0 : this.queues[domain];
|
|
419
|
+
if (domain === void 0 || !lock || !queue) throw new Error(`No Bluetooth reply queue for domain ${domain}.`);
|
|
420
|
+
return lock.run(async () => {
|
|
421
|
+
const frame = require_framing.prependLength(_teslemetry_tesla_protocol_command_universal_message.RoutableMessage.encode(msg).finish());
|
|
422
|
+
for (const stale of queue.drain()) this.log(`BLE discarded a stale reply from domain ${stale.fromDestination?.domain}`);
|
|
423
|
+
await this.connect_if_needed();
|
|
424
|
+
const link = this.link;
|
|
425
|
+
if (!link?.connection.isConnected) {
|
|
426
|
+
if (link) this.loseLink(link, "backend reports it down");
|
|
427
|
+
throw new require_exceptions.BluetoothTransportError();
|
|
428
|
+
}
|
|
429
|
+
const watcher = confirmBroadcast && !optimistic ? this.armBroadcastWatcher(domain, confirmBroadcast) : null;
|
|
430
|
+
const awaitReply = (signal) => this.awaitReply(queue, msg, requires, expectsData, timeoutMs, signal);
|
|
431
|
+
const awaitOutcome = () => watcher ? this.awaitReplyOrBroadcast(link, watcher, awaitReply) : awaitReply(link.closed.signal);
|
|
432
|
+
try {
|
|
433
|
+
try {
|
|
434
|
+
await this.writeFrame(link, frame, signal);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (signal?.aborted && error === signal.reason) throw error;
|
|
437
|
+
if (error instanceof require_transport.BleNotSubmittedError) {
|
|
438
|
+
if (!link.connection.isConnected) this.loseLink(link, describe(error));
|
|
439
|
+
throw new require_exceptions.BluetoothTransportError(null, null, { cause: error });
|
|
440
|
+
}
|
|
441
|
+
onSubmitted?.();
|
|
442
|
+
if (watcher) watcher.writeEnded = true;
|
|
443
|
+
if (optimistic) {
|
|
444
|
+
this.loseLink(link, `write failed: ${describe(error)}`);
|
|
445
|
+
throw new require_exceptions.BluetoothTimeout(null, null, { cause: error });
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
return await awaitOutcome();
|
|
449
|
+
} catch (timeout) {
|
|
450
|
+
if (!(timeout instanceof require_exceptions.BluetoothTimeout)) throw timeout;
|
|
451
|
+
this.loseLink(link, `write failed: ${describe(error)}`);
|
|
452
|
+
throw new require_exceptions.BluetoothTimeout(timeout.data, timeout.status, { cause: error });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
onSubmitted?.();
|
|
456
|
+
if (watcher) {
|
|
457
|
+
watcher.writeEnded = true;
|
|
458
|
+
watcher.writeComplete = true;
|
|
459
|
+
}
|
|
460
|
+
if (optimistic) return _teslemetry_tesla_protocol_command_universal_message.RoutableMessage.create({});
|
|
461
|
+
return await awaitOutcome();
|
|
462
|
+
} finally {
|
|
463
|
+
if (watcher && this.broadcastWatchers[domain] === watcher) delete this.broadcastWatchers[domain];
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Watch for broadcasts showing the requested state. A lost ack does not stop
|
|
469
|
+
* the car broadcasting the change it caused. Only a status after the write
|
|
470
|
+
* counts, as one from before it says nothing.
|
|
471
|
+
*/
|
|
472
|
+
armBroadcastWatcher(domain, matches) {
|
|
473
|
+
let resolve;
|
|
474
|
+
const watcher = {
|
|
475
|
+
confirmed: new Promise((done) => {
|
|
476
|
+
resolve = done;
|
|
477
|
+
}),
|
|
478
|
+
mismatched: false,
|
|
479
|
+
writeEnded: false,
|
|
480
|
+
writeComplete: false,
|
|
481
|
+
onBroadcast: (message) => {
|
|
482
|
+
const status = require_broadcast.decodeVcsecStatus(message);
|
|
483
|
+
if (status === null || watcher.confirmation || !watcher.writeEnded) return;
|
|
484
|
+
const executed = matches(status);
|
|
485
|
+
if (executed) {
|
|
486
|
+
watcher.confirmation = message;
|
|
487
|
+
resolve(message);
|
|
488
|
+
} else if (executed === false && watcher.writeComplete) watcher.mismatched = true;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
this.broadcastWatchers[domain] = watcher;
|
|
492
|
+
return watcher;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Race the addressed reply against a matching broadcast. A mismatch does not
|
|
496
|
+
* fail fast, because it may predate the actuation and a later broadcast can
|
|
497
|
+
* still confirm it. Only a mismatch still standing when the whole window has
|
|
498
|
+
* run out proves the command did not apply, and a lost link cuts the window
|
|
499
|
+
* short, so it proves nothing then.
|
|
500
|
+
*/
|
|
501
|
+
async awaitReplyOrBroadcast(link, watcher, awaitReply) {
|
|
502
|
+
const raceOver = new AbortController();
|
|
503
|
+
const reply = awaitReply(AbortSignal.any([link.closed.signal, raceOver.signal]));
|
|
504
|
+
try {
|
|
505
|
+
const winner = await Promise.race([reply.then(() => "reply", () => "reply"), watcher.confirmed]);
|
|
506
|
+
if (winner !== "reply") return winner;
|
|
507
|
+
return await reply;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (error instanceof require_exceptions.BluetoothTimeout) {
|
|
510
|
+
if (watcher.confirmation) return watcher.confirmation;
|
|
511
|
+
if (watcher.mismatched && !link.lost) throw new require_exceptions.BluetoothCommandFailed(error.data, error.status, { cause: error });
|
|
512
|
+
}
|
|
513
|
+
throw error;
|
|
514
|
+
} finally {
|
|
515
|
+
raceOver.abort();
|
|
516
|
+
await reply.catch(() => void 0);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Wait for the reply carrying `requires`, or a bare ack of this request.
|
|
521
|
+
* @param signal Ends the wait early, as a timeout does.
|
|
522
|
+
*/
|
|
523
|
+
async awaitReply(queue, msg, requires, expectsData, timeoutMs, signal) {
|
|
524
|
+
const uuid = Buffer.from(msg.uuid);
|
|
525
|
+
const domain = msg.toDestination?.domain;
|
|
526
|
+
const hasRequired = (resp) => (requires === "session_info" ? resp.sessionInfo : resp.protobufMessageAsBytes) !== void 0;
|
|
527
|
+
const deadline = performance.now() + timeoutMs;
|
|
528
|
+
for (;;) {
|
|
529
|
+
const resp = await queue.get(deadline - performance.now(), signal);
|
|
530
|
+
if (resp === null) throw new require_exceptions.BluetoothTimeout();
|
|
531
|
+
if (this.isForeignReply(resp, msg, expectsData)) {
|
|
532
|
+
this.log(`BLE ignored a reply that is not for this request, domain ${resp.fromDestination?.domain}`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
this.validate_msg(resp, uuid, domain);
|
|
536
|
+
if (hasRequired(resp)) return resp;
|
|
537
|
+
if (uuid.length === 0 || !uuid.equals(resp.requestUuid)) continue;
|
|
538
|
+
if (!expectsData) return resp;
|
|
539
|
+
const followupDeadline = performance.now() + this.ackFollowupTimeoutMs;
|
|
540
|
+
for (;;) {
|
|
541
|
+
const limit = Math.min(followupDeadline, deadline);
|
|
542
|
+
const followup = await queue.get(limit - performance.now(), signal);
|
|
543
|
+
if (followup === null) {
|
|
544
|
+
if (followupDeadline > deadline && !signal.aborted) throw new require_exceptions.BluetoothTimeout();
|
|
545
|
+
return resp;
|
|
546
|
+
}
|
|
547
|
+
if (this.isForeignReply(followup, msg, expectsData)) {
|
|
548
|
+
this.log(`BLE ignored a reply that is not for this request, domain ${followup.fromDestination?.domain}`);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
this.validate_msg(followup, uuid, domain);
|
|
552
|
+
if (hasRequired(followup)) return followup;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Whether a reply must be ignored rather than acted on. The link is shared, so
|
|
558
|
+
* an addressed reply can be a late answer to an earlier request, and acting on
|
|
559
|
+
* one can resend a command or report one that never ran.
|
|
560
|
+
*
|
|
561
|
+
* A reply naming another request is never ours. One naming no request must
|
|
562
|
+
* still be heard, because real VCSEC leaves `request_uuid` empty, but nothing
|
|
563
|
+
* ties it to this request unless its session-info tag or its encryption does,
|
|
564
|
+
* and both of those are checked later. So it may ack a command or answer a
|
|
565
|
+
* read, and it may not make anything happen: not a resend or a fault, and a
|
|
566
|
+
* status does not stand in for an actuation's ack.
|
|
567
|
+
*/
|
|
568
|
+
isForeignReply(resp, msg, expectsData) {
|
|
569
|
+
if (resp.requestUuid.length > 0) return !Buffer.from(msg.uuid).equals(resp.requestUuid);
|
|
570
|
+
if (msg.sessionInfoRequest || resp.sessionInfo && resp.sessionInfo.length > 0) return false;
|
|
571
|
+
const status = resp.signedMessageStatus;
|
|
572
|
+
if (status?.operationStatus === _teslemetry_tesla_protocol_command_universal_message.OperationStatusE.OPERATIONSTATUS_WAIT || status && status.signedMessageFault !== _teslemetry_tesla_protocol_command_universal_message.MessageFaultE.MESSAGEFAULT_ERROR_NONE) return true;
|
|
573
|
+
if (msg.toDestination?.domain !== VCSEC || !resp.protobufMessageAsBytes || resp.signatureData?.AESGCMResponseData) return false;
|
|
574
|
+
try {
|
|
575
|
+
const reply = _teslemetry_tesla_protocol_command_vcsec.FromVCSECMessage.decode(resp.protobufMessageAsBytes);
|
|
576
|
+
return reply.commandStatus?.operationStatus === _teslemetry_tesla_protocol_command_vcsec.OperationStatusE.OPERATIONSTATUS_WAIT || !expectsData && reply.vehicleStatus !== void 0;
|
|
577
|
+
} catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/** A session that a reply left unready cannot sign, and that reply proved nothing. */
|
|
582
|
+
async ensureHandshake(domain) {
|
|
583
|
+
await super.ensureHandshake(domain);
|
|
584
|
+
if (!this.sessionFor(domain).ready) throw new require_exceptions.BluetoothTimeout();
|
|
585
|
+
}
|
|
586
|
+
/** A failed handshake drops the link, so a retry starts on a fresh one. */
|
|
587
|
+
async handshakeVehicleSecurity() {
|
|
588
|
+
try {
|
|
589
|
+
await super.handshakeVehicleSecurity();
|
|
590
|
+
} catch (error) {
|
|
591
|
+
await this.dropLinkOnFailure("vehicle security handshake failed", error);
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* A failed handshake drops the link, unless VCSEC reports the car asleep:
|
|
597
|
+
* its infotainment is meant to be silent then, and that read proves the link.
|
|
598
|
+
*/
|
|
599
|
+
async handshakeInfotainment() {
|
|
600
|
+
try {
|
|
601
|
+
await super.handshakeInfotainment();
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (!(error instanceof require_exceptions.BluetoothTimeout)) await this.dropLinkOnFailure("infotainment handshake failed", error);
|
|
604
|
+
else if (!await this.vehicleAsleep()) await this.dropLink("infotainment handshake failed");
|
|
605
|
+
throw error;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Sign and write a mutating command, with no reply wait. A write that fails
|
|
610
|
+
* provably before submission still raises. One that entered the backend may
|
|
611
|
+
* have reached the car, so even here it must not let a router replay it, and
|
|
612
|
+
* it follows `raise_unconfirmed`.
|
|
613
|
+
*/
|
|
614
|
+
async sendOptimistic(domain, command, name) {
|
|
615
|
+
const session = this.sessionFor(domain);
|
|
616
|
+
const send = (msg) => this._send(msg, "protobuf_message_as_bytes", { optimistic: true });
|
|
617
|
+
try {
|
|
618
|
+
if (domain === VCSEC) await session.withLock(() => send(this.buildMessage(session, command)));
|
|
619
|
+
else await send(await session.withLock(() => this.buildMessage(session, command)));
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (!(error instanceof require_exceptions.BluetoothTimeout)) throw error;
|
|
622
|
+
return this.unconfirmedOutcome(name, new require_exceptions.BluetoothUnconfirmedCommand(error.data, error.status, { cause: error }));
|
|
623
|
+
}
|
|
624
|
+
return success();
|
|
625
|
+
}
|
|
626
|
+
/** End a ladder that could not determine what happened. */
|
|
627
|
+
unconfirmedOutcome(name, unconfirmed) {
|
|
628
|
+
if (this.raise_unconfirmed) throw unconfirmed;
|
|
629
|
+
this.log(`command=${name} transport=${this._transport_name} raise_unconfirmed=False result=success (best-effort)`);
|
|
630
|
+
return success();
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Confirm a timed-out command by state, when `"verify"` asks for it.
|
|
634
|
+
* @returns Success, or `null` if the state could not be read. An unreadable car is never woken to verify.
|
|
635
|
+
* @throws BluetoothCommandFailed when the state shows it did not apply, whatever `raise_unconfirmed` says.
|
|
636
|
+
*/
|
|
637
|
+
async verifyTimeout(name, plan, timeout) {
|
|
638
|
+
if (this.confirmation !== "verify" || !plan) return null;
|
|
639
|
+
let state;
|
|
640
|
+
try {
|
|
641
|
+
state = await this[plan.reader]();
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (error instanceof require_exceptions.TeslaFleetError) {
|
|
644
|
+
this.log(`command=${name} transport=${this._transport_name} verify_commands=unresolved`);
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
const executed = plan.executed(state);
|
|
650
|
+
if (executed === void 0) {
|
|
651
|
+
this.log(`command=${name} transport=${this._transport_name} verify_commands=unresolved`);
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
this.log(`command=${name} transport=${this._transport_name} verify_commands=resolved result=${executed}`);
|
|
655
|
+
if (!executed) throw new require_exceptions.BluetoothCommandFailed(timeout.data, timeout.status, { cause: timeout });
|
|
656
|
+
return success();
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Open the flap or release a cable. Under `"verify"`, the charging subsystem
|
|
660
|
+
* is made ready first, and only physical state counts as success afterwards:
|
|
661
|
+
* an opening has been reported waking a sleeping car without releasing the
|
|
662
|
+
* latch. No readiness failure has sent anything, so a router may fail over on
|
|
663
|
+
* any of them. Once the one opening write has gone, an unresolved result always
|
|
664
|
+
* raises `BluetoothUnconfirmedCommand`, whatever `raise_unconfirmed` says, and
|
|
665
|
+
* it is never sent again: a best-effort success would hide a latch that never
|
|
666
|
+
* released, and a replay could release a cable the caller had plugged back in.
|
|
667
|
+
* Other modes keep the ordinary VCSEC ladder.
|
|
668
|
+
* @throws BluetoothCommandFailed when the car is asleep with `wake_if_asleep` off, or the cable state is unknown.
|
|
669
|
+
* @throws BluetoothTimeout when the car is not ready in time.
|
|
670
|
+
* @throws BluetoothUnconfirmedCommand when physical state never showed the opening.
|
|
671
|
+
*/
|
|
672
|
+
async charge_port_door_open() {
|
|
673
|
+
if (this.confirmation !== "verify") return super.charge_port_door_open();
|
|
674
|
+
const connected = await this.chargePortPreflight();
|
|
675
|
+
let cause;
|
|
676
|
+
let submitted = false;
|
|
677
|
+
try {
|
|
678
|
+
const result = await super.sendVehicleSecurity(_teslemetry_tesla_protocol_command_vcsec.UnsignedMessage.create({ closureMoveRequest: _teslemetry_tesla_protocol_command_vcsec.ClosureMoveRequest.create({ chargePort: _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_OPEN }) }), { onSubmitted: () => submitted = true });
|
|
679
|
+
if (result.response.result === false) return result;
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (!submitted || !leavesOutcomeUnknown(error)) throw error;
|
|
682
|
+
cause = error;
|
|
683
|
+
}
|
|
684
|
+
return this.verifyChargePortOpened(connected, cause);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Everything before the opening, under one `wakeTimeoutMs` deadline.
|
|
688
|
+
* @returns Whether a cable is connected, which picks the proof of success.
|
|
689
|
+
*/
|
|
690
|
+
async chargePortPreflight() {
|
|
691
|
+
const deadline = performance.now() + this.wakeTimeoutMs;
|
|
692
|
+
const over = new AbortController();
|
|
693
|
+
try {
|
|
694
|
+
const connected = await require_async.withTimeout(() => this.chargePortReadiness(over.signal), this.wakeTimeoutMs);
|
|
695
|
+
try {
|
|
696
|
+
await require_async.withTimeout(() => this.ensureHandshake(VCSEC), deadline - performance.now());
|
|
697
|
+
} catch (error) {
|
|
698
|
+
await this.dropLinkOnFailure("vehicle security handshake failed", error);
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
return connected;
|
|
702
|
+
} catch (error) {
|
|
703
|
+
if (error instanceof require_async.BleCallTimeout) throw new require_exceptions.BluetoothTimeout("Charge-port readiness timed out", null, { cause: error });
|
|
704
|
+
throw error;
|
|
705
|
+
} finally {
|
|
706
|
+
over.abort();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async chargePortReadiness(over) {
|
|
710
|
+
if (await this.vehicleAsleep()) {
|
|
711
|
+
if (!this.wake_if_asleep) throw new require_exceptions.BluetoothCommandFailed("Vehicle asleep; wake disabled");
|
|
712
|
+
await this.wakeForCommand(new require_exceptions.BluetoothTimeout());
|
|
713
|
+
} else await this.awaitInfotainment(this.infotainmentBootTimeoutMs, false, over);
|
|
714
|
+
for (;;) {
|
|
715
|
+
try {
|
|
716
|
+
const connected = require_verify.chargePortConnected(await this.charge_state());
|
|
717
|
+
if (connected === null) throw new require_exceptions.BluetoothCommandFailed("Charge-port cable state unknown; no opening command sent");
|
|
718
|
+
return connected;
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (error instanceof require_exceptions.InvalidResponse) throw new require_exceptions.BluetoothCommandFailed("Charge-port cable state unknown; no opening command sent", null, { cause: error });
|
|
721
|
+
if (!(error instanceof require_exceptions.BluetoothTimeout) || over.aborted) throw error;
|
|
722
|
+
}
|
|
723
|
+
await require_async.sleep(this.chargePortVerifyIntervalMs);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Poll for the physical result, holding to the goal set before the opening:
|
|
728
|
+
* an open flap must never stand in for a latch that did not release. Nothing
|
|
729
|
+
* here may send the opening again, and no failed read may reach a router as
|
|
730
|
+
* anything but unconfirmed.
|
|
731
|
+
*/
|
|
732
|
+
async verifyChargePortOpened(connected, opening) {
|
|
733
|
+
const name = "closureMoveRequest";
|
|
734
|
+
const log = (message) => this.log(message);
|
|
735
|
+
let cause = opening;
|
|
736
|
+
const deadline = performance.now() + this.chargePortVerifyTimeoutMs;
|
|
737
|
+
for (let remaining = this.chargePortVerifyTimeoutMs; remaining > 0; remaining = deadline - performance.now()) {
|
|
738
|
+
try {
|
|
739
|
+
const after = await require_async.withTimeout(() => this.charge_state(), remaining);
|
|
740
|
+
if (connected ? after.chargePortLatch?.Disengaged !== void 0 : require_verify.chargePortConnected(after) === false && after.chargePortDoorOpen === true) {
|
|
741
|
+
log(`command=${name} transport=${this._transport_name} physical_confirmation=confirmed`);
|
|
742
|
+
require_log.logRequestResult(log, name, this._transport_name, success());
|
|
743
|
+
return success();
|
|
744
|
+
}
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (!(error instanceof require_async.BleCallTimeout)) cause = error;
|
|
747
|
+
}
|
|
748
|
+
await require_async.sleep(Math.min(this.chargePortVerifyIntervalMs, Math.max(0, deadline - performance.now())));
|
|
749
|
+
}
|
|
750
|
+
const unconfirmed = new require_exceptions.BluetoothUnconfirmedCommand("Charge-port opening was not confirmed by physical state", null, { cause });
|
|
751
|
+
log(`command=${name} transport=${this._transport_name} physical_confirmation=unconfirmed`);
|
|
752
|
+
require_log.logRequestError(log, name, this._transport_name, unconfirmed);
|
|
753
|
+
throw unconfirmed;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Send a VCSEC actuation through the confirmation ladder. A plan's check also
|
|
757
|
+
* races the ack, so `options` is ignored: the plan is the only source of it.
|
|
758
|
+
*/
|
|
759
|
+
async sendVehicleSecurity(command, _options = {}) {
|
|
760
|
+
try {
|
|
761
|
+
await this.ensureHandshake(VCSEC);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
await this.dropLinkOnFailure("vehicle security handshake failed", error);
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
const name = require_commands.vcsec_command_name(command);
|
|
767
|
+
if (this.confirmation === "optimistic") return this.sendOptimistic(VCSEC, Buffer.from(_teslemetry_tesla_protocol_command_vcsec.UnsignedMessage.encode(command).finish()), name);
|
|
768
|
+
const plan = require_verify.vcsecVerifyPlan(command);
|
|
769
|
+
let submitted = false;
|
|
770
|
+
try {
|
|
771
|
+
return await super.sendVehicleSecurity(command, {
|
|
772
|
+
confirmBroadcast: plan?.executed,
|
|
773
|
+
onSubmitted: () => submitted = true
|
|
774
|
+
});
|
|
775
|
+
} catch (error) {
|
|
776
|
+
if (!submitted || !leavesOutcomeUnknown(error)) throw error;
|
|
777
|
+
const unconfirmed = new require_exceptions.BluetoothUnconfirmedCommand(null, null, { cause: error });
|
|
778
|
+
return await this.verifyTimeout(name, plan, unconfirmed) ?? this.unconfirmedOutcome(name, unconfirmed);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Send an infotainment command through the confirmation ladder. `mutating:
|
|
783
|
+
* false` is exempt: `ping` always waits for its real reply.
|
|
784
|
+
*/
|
|
785
|
+
async sendInfotainment(action, { mutating = true } = {}) {
|
|
786
|
+
return this.infotainmentLadder(action, mutating, this.wake_if_asleep);
|
|
787
|
+
}
|
|
788
|
+
async infotainmentLadder(action, mutating, wake) {
|
|
789
|
+
try {
|
|
790
|
+
await this.ensureHandshake(INFOTAINMENT);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
if (!(error instanceof require_exceptions.BluetoothTimeout)) {
|
|
793
|
+
await this.dropLinkOnFailure("infotainment handshake failed", error);
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
const sleepStatus = await this.sleepStatus();
|
|
797
|
+
if (sleepStatus === _teslemetry_tesla_protocol_command_vcsec.VehicleSleepStatusE.VEHICLE_SLEEP_STATUS_ASLEEP) {
|
|
798
|
+
if (!wake) throw error;
|
|
799
|
+
await this.wakeForCommand(error);
|
|
800
|
+
wake = false;
|
|
801
|
+
} else if (sleepStatus === _teslemetry_tesla_protocol_command_vcsec.VehicleSleepStatusE.VEHICLE_SLEEP_STATUS_AWAKE) try {
|
|
802
|
+
await this.awaitInfotainment(this.infotainmentBootTimeoutMs, false);
|
|
803
|
+
} catch (bootError) {
|
|
804
|
+
if (!(bootError instanceof require_exceptions.BluetoothTimeout)) throw bootError;
|
|
805
|
+
await this.dropLink("infotainment handshake failed");
|
|
806
|
+
throw error;
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
await this.dropLink("infotainment handshake failed");
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const name = require_commands.infotainment_command_name(action);
|
|
814
|
+
if (this.confirmation === "optimistic" && mutating) return this.sendOptimistic(INFOTAINMENT, Buffer.from(_teslemetry_tesla_protocol_command_car_server.Action.encode(action).finish()), name);
|
|
815
|
+
let submitted = false;
|
|
816
|
+
try {
|
|
817
|
+
return await super.sendInfotainment(action, { onSubmitted: () => submitted = true });
|
|
818
|
+
} catch (error) {
|
|
819
|
+
if (!mutating || !submitted || !leavesOutcomeUnknown(error)) throw error;
|
|
820
|
+
const unconfirmed = new require_exceptions.BluetoothUnconfirmedCommand(null, null, { cause: error });
|
|
821
|
+
const verified = await this.verifyTimeout(name, require_verify.infotainmentVerifyPlan(action.vehicleAction), unconfirmed);
|
|
822
|
+
if (verified) return verified;
|
|
823
|
+
if (!this.raise_unconfirmed && await this.vehicleAsleep()) {
|
|
824
|
+
if (!wake) throw new require_exceptions.BluetoothCommandFailed(null, null, { cause: error });
|
|
825
|
+
await this.wakeForCommand(unconfirmed);
|
|
826
|
+
return this.infotainmentLadder(action, mutating, false);
|
|
827
|
+
}
|
|
828
|
+
return this.unconfirmedOutcome(name, unconfirmed);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Wake a sleeping car until infotainment can take a command.
|
|
833
|
+
* @throws BluetoothCommandFailed when it cannot: the command provably never ran.
|
|
834
|
+
*/
|
|
835
|
+
async wakeForCommand(timeout) {
|
|
836
|
+
try {
|
|
837
|
+
await this.wake_up({
|
|
838
|
+
wait: true,
|
|
839
|
+
timeoutMs: this.wakeTimeoutMs
|
|
840
|
+
});
|
|
841
|
+
} catch (cause) {
|
|
842
|
+
if (cause instanceof require_exceptions.TeslaFleetError) throw new require_exceptions.BluetoothCommandFailed(timeout.data, timeout.status, { cause });
|
|
843
|
+
throw cause;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/** VCSEC's sleep status, or `null` if unreadable. */
|
|
847
|
+
async sleepStatus() {
|
|
848
|
+
try {
|
|
849
|
+
return (await this.vehicle_state()).vehicleSleepStatus;
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (error instanceof require_exceptions.TeslaFleetError) return null;
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/** Whether VCSEC reports the car asleep; `false` if unreadable. */
|
|
856
|
+
async vehicleAsleep() {
|
|
857
|
+
return await this.sleepStatus() === _teslemetry_tesla_protocol_command_vcsec.VehicleSleepStatusE.VEHICLE_SLEEP_STATUS_ASLEEP;
|
|
858
|
+
}
|
|
859
|
+
/** The vehicle security computer's `VehicleStatus`. It answers while the car sleeps. */
|
|
860
|
+
async vehicle_state() {
|
|
861
|
+
return this._getVehicleSecurity(_teslemetry_tesla_protocol_command_vcsec.UnsignedMessage.create({ InformationRequest: _teslemetry_tesla_protocol_command_vcsec.InformationRequest.create({ informationRequestType: _teslemetry_tesla_protocol_command_vcsec.InformationRequestType.INFORMATION_REQUEST_TYPE_GET_STATUS }) }));
|
|
862
|
+
}
|
|
863
|
+
async readVehicleData(request, field) {
|
|
864
|
+
const value = (await this._getInfotainment(_teslemetry_tesla_protocol_command_car_server.Action.create({ vehicleAction: _teslemetry_tesla_protocol_command_car_server.VehicleAction.create({ getVehicleData: _teslemetry_tesla_protocol_command_car_server.GetVehicleData.create(request) }) })))[field];
|
|
865
|
+
if (value === void 0 || value === null) throw new require_exceptions.InvalidResponse({ missing: field });
|
|
866
|
+
return value;
|
|
867
|
+
}
|
|
868
|
+
/** The charging state. */
|
|
869
|
+
async charge_state() {
|
|
870
|
+
return this.readVehicleData({ getChargeState: _teslemetry_tesla_protocol_command_car_server.GetChargeState.create({}) }, "chargeState");
|
|
871
|
+
}
|
|
872
|
+
/** The HVAC and cabin climate state. */
|
|
873
|
+
async climate_state() {
|
|
874
|
+
return this.readVehicleData({ getClimateState: _teslemetry_tesla_protocol_command_car_server.GetClimateState.create({}) }, "climateState");
|
|
875
|
+
}
|
|
876
|
+
/** The driving state, including speed and power. */
|
|
877
|
+
async drive_state() {
|
|
878
|
+
return this.readVehicleData({ getDriveState: _teslemetry_tesla_protocol_command_car_server.GetDriveState.create({}) }, "driveState");
|
|
879
|
+
}
|
|
880
|
+
/** The vehicle's location. */
|
|
881
|
+
async location_state() {
|
|
882
|
+
return this.readVehicleData({ getLocationState: _teslemetry_tesla_protocol_command_car_server.GetLocationState.create({}) }, "locationState");
|
|
883
|
+
}
|
|
884
|
+
/** The doors, windows, trunks and locks, as infotainment reports them. */
|
|
885
|
+
async closures_state() {
|
|
886
|
+
return this.readVehicleData({ getClosuresState: _teslemetry_tesla_protocol_command_car_server.GetClosuresState.create({}) }, "closuresState");
|
|
887
|
+
}
|
|
888
|
+
/** The charge schedules. */
|
|
889
|
+
async charge_schedule_state() {
|
|
890
|
+
return this.readVehicleData({ getChargeScheduleState: _teslemetry_tesla_protocol_command_car_server.GetChargeScheduleState.create({}) }, "chargeScheduleState");
|
|
891
|
+
}
|
|
892
|
+
/** The preconditioning schedules. */
|
|
893
|
+
async preconditioning_schedule_state() {
|
|
894
|
+
return this.readVehicleData({ getPreconditioningScheduleState: _teslemetry_tesla_protocol_command_car_server.GetPreconditioningScheduleState.create({}) }, "preconditioningScheduleState");
|
|
895
|
+
}
|
|
896
|
+
/** The tire pressures. */
|
|
897
|
+
async tire_pressure_state() {
|
|
898
|
+
return this.readVehicleData({ getTirePressureState: _teslemetry_tesla_protocol_command_car_server.GetTirePressureState.create({}) }, "tirePressureState");
|
|
899
|
+
}
|
|
900
|
+
/** The media playback state. */
|
|
901
|
+
async media_state() {
|
|
902
|
+
return this.readVehicleData({ getMediaState: _teslemetry_tesla_protocol_command_car_server.GetMediaState.create({}) }, "mediaState");
|
|
903
|
+
}
|
|
904
|
+
/** Detail on the media playing now. */
|
|
905
|
+
async media_detail_state() {
|
|
906
|
+
return this.readVehicleData({ getMediaDetailState: _teslemetry_tesla_protocol_command_car_server.GetMediaDetailState.create({}) }, "mediaDetailState");
|
|
907
|
+
}
|
|
908
|
+
/** The software update state. */
|
|
909
|
+
async software_update_state() {
|
|
910
|
+
return this.readVehicleData({ getSoftwareUpdateState: _teslemetry_tesla_protocol_command_car_server.GetSoftwareUpdateState.create({}) }, "softwareUpdateState");
|
|
911
|
+
}
|
|
912
|
+
/** The parental controls state. */
|
|
913
|
+
async parental_controls_state() {
|
|
914
|
+
return this.readVehicleData({ getParentalControlsState: _teslemetry_tesla_protocol_command_car_server.GetParentalControlsState.create({}) }, "parentalControlsState");
|
|
915
|
+
}
|
|
916
|
+
/** The display units and formats. */
|
|
917
|
+
async gui_settings() {
|
|
918
|
+
return this.readVehicleData({ getGuiSettings: _teslemetry_tesla_protocol_command_car_server.GetGuiSettings.create({}) }, "guiSettings");
|
|
919
|
+
}
|
|
920
|
+
/** The state of parked accessories, such as a tent. */
|
|
921
|
+
async parked_accessory_state() {
|
|
922
|
+
return this.readVehicleData({ getParkedAccessoryState: _teslemetry_tesla_protocol_command_car_server.GetParkedAccessoryState.create({}) }, "parkedAccessoryState");
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* The older infotainment vehicle state, tag 6, for older firmware. Not `current_vehicle_state`, nor VCSEC's `vehicle_state`.
|
|
926
|
+
*/
|
|
927
|
+
async legacy_vehicle_state() {
|
|
928
|
+
return this.readVehicleData({ getLegacyVehicleState: _teslemetry_tesla_protocol_command_car_server.GetLegacyVehicleState.create({}) }, "legacyVehicleState");
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* The infotainment vehicle state, tag 18, on current firmware. Not `legacy_vehicle_state`, nor VCSEC's `vehicle_state`.
|
|
932
|
+
*/
|
|
933
|
+
async current_vehicle_state() {
|
|
934
|
+
return this.readVehicleData({ getVehicleState: _teslemetry_tesla_protocol_command_car_server.GetVehicleState.create({}) }, "vehicleState");
|
|
935
|
+
}
|
|
936
|
+
/** The vehicle's fixed configuration. */
|
|
937
|
+
async vehicle_config() {
|
|
938
|
+
return this.readVehicleData({ getVehicleConfig: _teslemetry_tesla_protocol_command_car_server.GetVehicleConfig.create({}) }, "vehicleConfig");
|
|
939
|
+
}
|
|
940
|
+
/** The battery state of health. */
|
|
941
|
+
async soh_state() {
|
|
942
|
+
return this.readVehicleData({ getSohState: _teslemetry_tesla_protocol_command_car_server.GetSohState.create({}) }, "sohState");
|
|
943
|
+
}
|
|
944
|
+
/** The vehicle's name and software details. */
|
|
945
|
+
async vehicle_detail_state() {
|
|
946
|
+
return this.readVehicleData({ getVehicleDetailState: _teslemetry_tesla_protocol_command_car_server.GetVehicleDetailState.create({}) }, "vehicleDetailState");
|
|
947
|
+
}
|
|
948
|
+
/** The display state. */
|
|
949
|
+
async display_state() {
|
|
950
|
+
return this.readVehicleData({ getDisplayState: _teslemetry_tesla_protocol_command_car_server.GetDisplayState.create({}) }, "displayState");
|
|
951
|
+
}
|
|
952
|
+
/** The active alerts. */
|
|
953
|
+
async alert_state() {
|
|
954
|
+
return this.readVehicleData({ getAlertState: _teslemetry_tesla_protocol_command_car_server.GetAlertState.create({}) }, "alertState");
|
|
955
|
+
}
|
|
956
|
+
/** The light show state. */
|
|
957
|
+
async light_show_state() {
|
|
958
|
+
return this.readVehicleData({ getLightShowState: _teslemetry_tesla_protocol_command_car_server.GetLightShowState.create({}) }, "lightShowState");
|
|
959
|
+
}
|
|
960
|
+
/** The suspension state. */
|
|
961
|
+
async suspension_state() {
|
|
962
|
+
return this.readVehicleData({ getSuspensionState: _teslemetry_tesla_protocol_command_car_server.GetSuspensionState.create({}) }, "suspensionState");
|
|
963
|
+
}
|
|
964
|
+
/** The child presence detection state. */
|
|
965
|
+
async child_presence_detection_state() {
|
|
966
|
+
return this.readVehicleData({ getChildPresenceDetectionState: _teslemetry_tesla_protocol_command_car_server.GetChildPresenceDetectionState.create({}) }, "childPresenceDetectionState");
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Several sub-states in one signed read. This is not the cloud method: it
|
|
970
|
+
* takes `BluetoothVehicleData` values, not `VehicleDataEndpoint`, and resolves
|
|
971
|
+
* to `VehicleData`, not a REST envelope. There is no default of every endpoint,
|
|
972
|
+
* because as few as two together have exceeded the response size limit, which
|
|
973
|
+
* raises `TeslaFleetMessageFaultResponseSizeExceedsMTU`. Prefer one reader each.
|
|
974
|
+
* @param endpoints An explicit list. `[]` asks for nothing.
|
|
975
|
+
* @throws BluetoothCloudUnavailableError for anything else, sending nothing: no argument and REST endpoint names are the cloud call, which a router can fail over to.
|
|
976
|
+
*/
|
|
977
|
+
async vehicle_data(endpoints) {
|
|
978
|
+
const known = Object.values(require_const.BluetoothVehicleData);
|
|
979
|
+
if (!Array.isArray(endpoints) || !endpoints.every((endpoint) => known.includes(endpoint))) throw new require_exceptions.BluetoothCloudUnavailableError();
|
|
980
|
+
const request = {};
|
|
981
|
+
for (const endpoint of endpoints) request[`${endpoint[0].toLowerCase()}${endpoint.slice(1)}`] = {};
|
|
982
|
+
return this._getInfotainment(_teslemetry_tesla_protocol_command_car_server.Action.create({ vehicleAction: _teslemetry_tesla_protocol_command_car_server.VehicleAction.create({ getVehicleData: _teslemetry_tesla_protocol_command_car_server.GetVehicleData.create(request) }) }));
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* The name set in the vehicle, read from GATT Device Name. It is not advertised.
|
|
986
|
+
* The vehicle prefixes it with a key emoji once it is available.
|
|
987
|
+
* @returns The name, or `null` if it never appeared: a backend may hide Generic Access.
|
|
988
|
+
*/
|
|
989
|
+
async query_display_name(max_attempts = 5) {
|
|
990
|
+
for (let attempt = 1; attempt <= max_attempts; attempt++) try {
|
|
991
|
+
await this.connect_if_needed();
|
|
992
|
+
const name = Buffer.from(await this.gattRead("deviceName")).toString("utf8");
|
|
993
|
+
if (name.startsWith(DISPLAY_NAME_PREFIX)) return name.slice(3);
|
|
994
|
+
await require_async.sleep(DISPLAY_NAME_RETRY_MS);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
this.log(`BLE display name read failed: ${describe(error)}`);
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
/** GATT Appearance, or `null` if it cannot be read. */
|
|
1001
|
+
async query_appearance() {
|
|
1002
|
+
try {
|
|
1003
|
+
await this.connect_if_needed();
|
|
1004
|
+
return await this.gattRead("appearance");
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
this.log(`BLE appearance read failed: ${describe(error)}`);
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/** The version characteristic as a big-endian integer, or `null` if it cannot be read. */
|
|
1011
|
+
async query_version() {
|
|
1012
|
+
try {
|
|
1013
|
+
await this.connect_if_needed();
|
|
1014
|
+
const version = await this.gattRead("version");
|
|
1015
|
+
return version.length === 0 ? null : version.reduce((value, byte) => value * 256 + byte, 0);
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
this.log(`BLE version read failed: ${describe(error)}`);
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
moveClosure(closure, move) {
|
|
1022
|
+
return this.sendVehicleSecurity(_teslemetry_tesla_protocol_command_vcsec.UnsignedMessage.create({ closureMoveRequest: _teslemetry_tesla_protocol_command_vcsec.ClosureMoveRequest.create({ [closure]: move }) }));
|
|
1023
|
+
}
|
|
1024
|
+
/** Opens the front driver door. */
|
|
1025
|
+
async open_front_driver_door() {
|
|
1026
|
+
return this.moveClosure("frontDriverDoor", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_OPEN);
|
|
1027
|
+
}
|
|
1028
|
+
/** Closes the front driver door. Doors have no reliable powered close on a Model 3. */
|
|
1029
|
+
async close_front_driver_door() {
|
|
1030
|
+
return this.moveClosure("frontDriverDoor", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_CLOSE);
|
|
1031
|
+
}
|
|
1032
|
+
/** Opens the front passenger door. */
|
|
1033
|
+
async open_front_passenger_door() {
|
|
1034
|
+
return this.moveClosure("frontPassengerDoor", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_OPEN);
|
|
1035
|
+
}
|
|
1036
|
+
/** Closes the front passenger door. Doors have no reliable powered close on a Model 3. */
|
|
1037
|
+
async close_front_passenger_door() {
|
|
1038
|
+
return this.moveClosure("frontPassengerDoor", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_CLOSE);
|
|
1039
|
+
}
|
|
1040
|
+
/** Opens the rear driver door. */
|
|
1041
|
+
async open_rear_driver_door() {
|
|
1042
|
+
return this.moveClosure("rearDriverDoor", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_OPEN);
|
|
1043
|
+
}
|
|
1044
|
+
/** Closes the rear driver door. Doors have no reliable powered close on a Model 3. */
|
|
1045
|
+
async close_rear_driver_door() {
|
|
1046
|
+
return this.moveClosure("rearDriverDoor", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_CLOSE);
|
|
1047
|
+
}
|
|
1048
|
+
/** Opens the rear passenger door. */
|
|
1049
|
+
async open_rear_passenger_door() {
|
|
1050
|
+
return this.moveClosure("rearPassengerDoor", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_OPEN);
|
|
1051
|
+
}
|
|
1052
|
+
/** Closes the rear passenger door. Doors have no reliable powered close on a Model 3. */
|
|
1053
|
+
async close_rear_passenger_door() {
|
|
1054
|
+
return this.moveClosure("rearPassengerDoor", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_CLOSE);
|
|
1055
|
+
}
|
|
1056
|
+
/** Opens the rear trunk. */
|
|
1057
|
+
async open_rear_trunk() {
|
|
1058
|
+
return this.moveClosure("rearTrunk", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_OPEN);
|
|
1059
|
+
}
|
|
1060
|
+
/** Closes the rear trunk. */
|
|
1061
|
+
async close_rear_trunk() {
|
|
1062
|
+
return this.moveClosure("rearTrunk", _teslemetry_tesla_protocol_command_vcsec.ClosureMoveTypeE.CLOSURE_MOVE_TYPE_CLOSE);
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Ask the vehicle to add this instance's key, which someone confirms by tapping
|
|
1066
|
+
* a key card on the console. Resolves once the key is on the whitelist.
|
|
1067
|
+
*
|
|
1068
|
+
* The whitelist operation is written exactly once, because a resend prompts
|
|
1069
|
+
* the user again. Its reply is one frame, which a link that cycles during the
|
|
1070
|
+
* walk to the car can lose, so completion is also proved by polling: a VCSEC
|
|
1071
|
+
* handshake only succeeds once this key is whitelisted, and it reconnects, so
|
|
1072
|
+
* the wait survives a dropped link.
|
|
1073
|
+
*
|
|
1074
|
+
* Success needs positive evidence: an explicit success reply, or an
|
|
1075
|
+
* authenticated handshake with this key. Python takes any reply without a
|
|
1076
|
+
* whitelist fault, which a WAIT or a bare ack satisfies before the tap.
|
|
1077
|
+
*
|
|
1078
|
+
* Tesla's Go SDK sends a bare `ToVCSECMessage` here. This wraps the
|
|
1079
|
+
* `UnsignedMessage` in a `RoutableMessage`, as Python does, and that has paired
|
|
1080
|
+
* a real car. If pairing fails on another vehicle, look there first.
|
|
1081
|
+
* @throws RangeError when `pollIntervalMs` is not positive, before anything is sent.
|
|
1082
|
+
* @throws SigningDisabled when this instance has no key, before anything is sent.
|
|
1083
|
+
* @throws BluetoothTimeout when nothing confirmed it by `timeoutMs`, with the link dropped.
|
|
1084
|
+
* @throws WhitelistOperationStatus or a subclass when the vehicle refused the key.
|
|
1085
|
+
* @throws The reason of `signal`, once it aborts. The link is kept.
|
|
1086
|
+
*/
|
|
1087
|
+
async pair({ role = _teslemetry_tesla_protocol_command_keys.Role.ROLE_OWNER, form = _teslemetry_tesla_protocol_command_vcsec.KeyFormFactor.KEY_FORM_FACTOR_CLOUD_KEY, timeoutMs = require_constants.DEFAULT_PAIR_TIMEOUT_MS, pollIntervalMs = require_constants.DEFAULT_PAIR_POLL_INTERVAL_MS, signal } = {}) {
|
|
1088
|
+
if (pollIntervalMs <= 0) throw new RangeError("pollIntervalMs must be greater than 0");
|
|
1089
|
+
if (this.publicKey.length === 0) throw new require_exceptions.SigningDisabled();
|
|
1090
|
+
signal?.throwIfAborted();
|
|
1091
|
+
const request = _teslemetry_tesla_protocol_command_vcsec.UnsignedMessage.create({ WhitelistOperation: _teslemetry_tesla_protocol_command_vcsec.WhitelistOperation.fromPartial({
|
|
1092
|
+
addKeyToWhitelistAndAddPermissions: {
|
|
1093
|
+
key: { PublicKeyRaw: this.publicKey },
|
|
1094
|
+
keyRole: role
|
|
1095
|
+
},
|
|
1096
|
+
metadataForKey: { keyFormFactor: form }
|
|
1097
|
+
}) });
|
|
1098
|
+
const msg = _teslemetry_tesla_protocol_command_universal_message.RoutableMessage.create({
|
|
1099
|
+
toDestination: { domain: VCSEC },
|
|
1100
|
+
fromDestination: { routingAddress: this.fromDestination },
|
|
1101
|
+
protobufMessageAsBytes: _teslemetry_tesla_protocol_command_vcsec.UnsignedMessage.encode(request).finish(),
|
|
1102
|
+
uuid: crypto.default.randomBytes(16)
|
|
1103
|
+
});
|
|
1104
|
+
try {
|
|
1105
|
+
await this.confirmPairing(msg, timeoutMs, pollIntervalMs, signal);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
if (!signal?.aborted) await this.dropLinkOnFailure("pairing failed", error);
|
|
1108
|
+
throw error;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async confirmPairing(msg, timeoutMs, pollIntervalMs, signal) {
|
|
1112
|
+
const deadline = performance.now() + timeoutMs;
|
|
1113
|
+
try {
|
|
1114
|
+
const send = this._send(msg, "protobuf_message_as_bytes", {
|
|
1115
|
+
timeoutMs: Math.max(0, Math.min(pollIntervalMs, deadline - performance.now())),
|
|
1116
|
+
signal
|
|
1117
|
+
});
|
|
1118
|
+
if (this.whitelistReplyConfirms(await require_async.abortable(send, signal))) return;
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
if (!(error instanceof require_exceptions.BluetoothTimeout)) throw error;
|
|
1121
|
+
}
|
|
1122
|
+
const confirmed = async () => this.consumeLateWhitelistReply() || await require_async.abortable(this.pairProbe(signal), signal);
|
|
1123
|
+
while (performance.now() < deadline) {
|
|
1124
|
+
signal?.throwIfAborted();
|
|
1125
|
+
if (await confirmed()) return;
|
|
1126
|
+
const remaining = deadline - performance.now();
|
|
1127
|
+
if (remaining <= 0) break;
|
|
1128
|
+
await require_async.sleep(Math.min(pollIntervalMs, remaining), signal);
|
|
1129
|
+
}
|
|
1130
|
+
signal?.throwIfAborted();
|
|
1131
|
+
if (await confirmed()) return;
|
|
1132
|
+
throw new require_exceptions.BluetoothTimeout();
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Whether this key is on the whitelist: a VCSEC handshake only completes once
|
|
1136
|
+
* it is. Any failure means "not yet", so polling carries on across a dropped
|
|
1137
|
+
* link. It sends one request, unlike `handshake`, because the next poll is the
|
|
1138
|
+
* retry, and nothing may be written after a `pair` is aborted.
|
|
1139
|
+
*/
|
|
1140
|
+
async pairProbe(signal) {
|
|
1141
|
+
const session = this.sessionFor(VCSEC);
|
|
1142
|
+
try {
|
|
1143
|
+
return await session.withLock(async () => {
|
|
1144
|
+
const reply = await this._send(this.sessionInfoRequest(VCSEC), "session_info", { signal });
|
|
1145
|
+
return session.ready && (reply.sessionInfo?.length ?? 0) > 0;
|
|
1146
|
+
});
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
if (error instanceof require_exceptions.TeslaFleetError) return false;
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Look for a whitelist reply that arrived after `_send` stopped waiting. This
|
|
1154
|
+
* must run before a probe, whose `_send` discards what is queued. Anything else
|
|
1155
|
+
* in the queue stays there.
|
|
1156
|
+
* @returns Whether one was there and explicitly reported success.
|
|
1157
|
+
* @throws WhitelistOperationStatus or a subclass when it reported a refusal.
|
|
1158
|
+
*/
|
|
1159
|
+
consumeLateWhitelistReply() {
|
|
1160
|
+
const queue = this.queues[VCSEC];
|
|
1161
|
+
let reply;
|
|
1162
|
+
for (const queued of queue.drain()) if (reply === void 0 && isWhitelistReply(queued)) reply = queued;
|
|
1163
|
+
else queue.push(queued);
|
|
1164
|
+
return reply !== void 0 && this.whitelistReplyConfirms(reply);
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Whether a reply to the whitelist operation says the key was added. Only an
|
|
1168
|
+
* OK with a whitelist status and no failure does. A WAIT means the car is
|
|
1169
|
+
* still waiting for the tap, and a bare ack, a status or anything else says
|
|
1170
|
+
* nothing, so `pair` goes on polling.
|
|
1171
|
+
* @throws WhitelistOperationStatus or a subclass when the reply is a refusal.
|
|
1172
|
+
*/
|
|
1173
|
+
whitelistReplyConfirms(reply) {
|
|
1174
|
+
let message;
|
|
1175
|
+
try {
|
|
1176
|
+
message = _teslemetry_tesla_protocol_command_vcsec.FromVCSECMessage.decode(reply.protobufMessageAsBytes ?? /* @__PURE__ */ new Uint8Array());
|
|
1177
|
+
} catch {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
const whitelist = message.commandStatus?.whitelistOperationStatus;
|
|
1181
|
+
this.raiseWhitelistFault(whitelist?.whitelistOperationInformation);
|
|
1182
|
+
const { OPERATIONSTATUS_ERROR, OPERATIONSTATUS_OK } = _teslemetry_tesla_protocol_command_vcsec.OperationStatusE;
|
|
1183
|
+
if (message.nominalError || message.commandStatus?.operationStatus === OPERATIONSTATUS_ERROR || whitelist?.operationStatus === OPERATIONSTATUS_ERROR) throw new require_exceptions.WhitelistOperationStatus("The vehicle refused the whitelist operation.");
|
|
1184
|
+
return message.commandStatus?.operationStatus === OPERATIONSTATUS_OK && whitelist?.operationStatus === OPERATIONSTATUS_OK;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Wake the vehicle. Takes `VehicleSpecific`'s options, but resolves to a
|
|
1188
|
+
* `CommandResponse`, not the REST `VehicleResponse`.
|
|
1189
|
+
*
|
|
1190
|
+
* Without `wait`, this sends the RKE wake to the vehicle security computer,
|
|
1191
|
+
* which acks well before infotainment can answer, so an infotainment command
|
|
1192
|
+
* sent straight after can still time out.
|
|
1193
|
+
*
|
|
1194
|
+
* With `wait`, it blocks until infotainment completes a fresh handshake, the
|
|
1195
|
+
* only proof it can take commands: VCSEC reports awake seconds before that.
|
|
1196
|
+
* The wake is idempotent, so it is resent every 3 s until then.
|
|
1197
|
+
* @param options.timeoutMs Deadline for `wait`, in milliseconds. Defaults to 30000.
|
|
1198
|
+
* @throws BluetoothTimeout at the deadline, with the link dropped.
|
|
1199
|
+
*/
|
|
1200
|
+
async wake_up({ wait = false, timeoutMs = require_constants.DEFAULT_WAKE_TIMEOUT_MS } = {}) {
|
|
1201
|
+
if (!wait) return this.sendVehicleSecurity(_teslemetry_tesla_protocol_command_vcsec.UnsignedMessage.create({ RKEAction: _teslemetry_tesla_protocol_command_vcsec.RKEActionE.RKE_ACTION_WAKE_VEHICLE }));
|
|
1202
|
+
try {
|
|
1203
|
+
await this.awaitInfotainment(timeoutMs, true);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
await this.dropLinkOnFailure("wake failed", error);
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
1208
|
+
return success();
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Probe infotainment until it answers. With `wake`, the RKE wake is sent
|
|
1212
|
+
* first and resent every `wakeResendIntervalMs`.
|
|
1213
|
+
* @param over Stops the probing, for a caller that has stopped waiting.
|
|
1214
|
+
* @throws BluetoothTimeout at the deadline.
|
|
1215
|
+
*/
|
|
1216
|
+
async awaitInfotainment(timeoutMs, wake, over) {
|
|
1217
|
+
const deadline = performance.now() + timeoutMs;
|
|
1218
|
+
let nextWake = performance.now();
|
|
1219
|
+
for (;;) {
|
|
1220
|
+
if (wake && performance.now() >= nextWake) {
|
|
1221
|
+
try {
|
|
1222
|
+
await this.wake_up();
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
if (!(error instanceof require_exceptions.BluetoothTimeout)) throw error;
|
|
1225
|
+
}
|
|
1226
|
+
nextWake = performance.now() + this.wakeResendIntervalMs;
|
|
1227
|
+
}
|
|
1228
|
+
const remaining = deadline - performance.now();
|
|
1229
|
+
if (remaining <= 0 || over?.aborted) throw new require_exceptions.BluetoothTimeout();
|
|
1230
|
+
try {
|
|
1231
|
+
await this.probeInfotainment(Math.min(this.wakeProbeTimeoutMs, remaining));
|
|
1232
|
+
return;
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
if (!(error instanceof require_exceptions.BluetoothTimeout || error instanceof require_exceptions.SessionInfoAuthenticationFault)) throw error;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Handshake with infotainment, always with a fresh request: only a live reply
|
|
1240
|
+
* proves it is awake, so a cached session counts for nothing.
|
|
1241
|
+
*/
|
|
1242
|
+
async probeInfotainment(timeoutMs) {
|
|
1243
|
+
if (this.publicKey.length === 0) throw new require_exceptions.SigningDisabled();
|
|
1244
|
+
await this._send(this.sessionInfoRequest(INFOTAINMENT), "session_info", { timeoutMs });
|
|
1245
|
+
if (!this.sessionFor(INFOTAINMENT).ready) throw new require_exceptions.BluetoothTimeout();
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
function isWhitelistReply(reply) {
|
|
1249
|
+
try {
|
|
1250
|
+
return _teslemetry_tesla_protocol_command_vcsec.FromVCSECMessage.decode(reply.protobufMessageAsBytes ?? /* @__PURE__ */ new Uint8Array()).commandStatus?.whitelistOperationStatus !== void 0;
|
|
1251
|
+
} catch {
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Whether a failure after a write says nothing about whether the command ran.
|
|
1257
|
+
* Only a car-side refusal rules that out, as the only `TeslaFleetError`s not
|
|
1258
|
+
* listed here are. A silent, unauthenticated, replayed or undecodable reply
|
|
1259
|
+
* proves nothing, a key fault may be a stale reply, a reply too big for the MTU
|
|
1260
|
+
* can follow execution, and a transport failure now belongs to a resend, after
|
|
1261
|
+
* the first frame was written. A router fails over on several of these, which is
|
|
1262
|
+
* right before a write and a double execution after one, so the ladders turn all
|
|
1263
|
+
* of them into an unresolved outcome first.
|
|
1264
|
+
*/
|
|
1265
|
+
function leavesOutcomeUnknown(error) {
|
|
1266
|
+
return !(error instanceof require_exceptions.TeslaFleetError) || error instanceof require_exceptions.BluetoothTimeout || error instanceof require_exceptions.BluetoothTransportError || error instanceof require_exceptions.NotOnWhitelistFault || error instanceof require_exceptions.SessionInfoAuthenticationFault || error instanceof require_exceptions.SignedCommandResponseReplayed || error instanceof require_exceptions.TeslaFleetMessageFaultResponseSizeExceedsMTU;
|
|
1267
|
+
}
|
|
1268
|
+
/** An error's class and message, for a log line. */
|
|
1269
|
+
function describe(error) {
|
|
1270
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
1271
|
+
}
|
|
1272
|
+
exports.default = VehicleBluetooth;
|