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.
Files changed (215) hide show
  1. package/README.md +269 -1
  2. package/dist/_virtual/_rolldown/runtime.cjs +10 -0
  3. package/dist/bluetooth/adapters/shared.cjs +38 -0
  4. package/dist/bluetooth/adapters/shared.mjs +36 -0
  5. package/dist/bluetooth/async.cjs +121 -0
  6. package/dist/bluetooth/async.d.cts +10 -0
  7. package/dist/bluetooth/async.d.mts +10 -0
  8. package/dist/bluetooth/async.mjs +116 -0
  9. package/dist/bluetooth/broadcast.cjs +153 -0
  10. package/dist/bluetooth/broadcast.d.cts +59 -0
  11. package/dist/bluetooth/broadcast.d.mts +59 -0
  12. package/dist/bluetooth/broadcast.mjs +151 -0
  13. package/dist/bluetooth/constants.cjs +96 -0
  14. package/dist/bluetooth/constants.d.cts +68 -0
  15. package/dist/bluetooth/constants.d.mts +68 -0
  16. package/dist/bluetooth/constants.mjs +70 -0
  17. package/dist/bluetooth/framing.cjs +125 -0
  18. package/dist/bluetooth/framing.d.cts +45 -0
  19. package/dist/bluetooth/framing.d.mts +45 -0
  20. package/dist/bluetooth/framing.mjs +122 -0
  21. package/dist/bluetooth/homey.cjs +165 -0
  22. package/dist/bluetooth/homey.d.cts +84 -0
  23. package/dist/bluetooth/homey.d.mts +84 -0
  24. package/dist/bluetooth/homey.mjs +159 -0
  25. package/dist/bluetooth/names.cjs +24 -0
  26. package/dist/bluetooth/names.d.cts +10 -0
  27. package/dist/bluetooth/names.d.mts +10 -0
  28. package/dist/bluetooth/names.mjs +20 -0
  29. package/dist/bluetooth/noble.cjs +178 -0
  30. package/dist/bluetooth/noble.d.cts +58 -0
  31. package/dist/bluetooth/noble.d.mts +58 -0
  32. package/dist/bluetooth/noble.mjs +177 -0
  33. package/dist/bluetooth/streamglue.cjs +157 -0
  34. package/dist/bluetooth/streamglue.d.cts +37 -0
  35. package/dist/bluetooth/streamglue.d.mts +37 -0
  36. package/dist/bluetooth/streamglue.mjs +154 -0
  37. package/dist/bluetooth/teslabluetooth.cjs +67 -0
  38. package/dist/bluetooth/teslabluetooth.d.cts +41 -0
  39. package/dist/bluetooth/teslabluetooth.d.mts +41 -0
  40. package/dist/bluetooth/teslabluetooth.mjs +66 -0
  41. package/dist/bluetooth/transport.cjs +10 -0
  42. package/dist/bluetooth/transport.d.cts +97 -0
  43. package/dist/bluetooth/transport.d.mts +97 -0
  44. package/dist/bluetooth/transport.mjs +10 -0
  45. package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
  46. package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
  47. package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
  48. package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
  49. package/dist/bluetooth/verify.cjs +93 -0
  50. package/dist/bluetooth/verify.d.cts +35 -0
  51. package/dist/bluetooth/verify.d.mts +35 -0
  52. package/dist/bluetooth/verify.mjs +91 -0
  53. package/dist/bluetooth.cjs +52 -0
  54. package/dist/bluetooth.d.cts +10 -0
  55. package/dist/bluetooth.d.mts +10 -0
  56. package/dist/bluetooth.mjs +9 -0
  57. package/dist/commands.cjs +1080 -85
  58. package/dist/commands.d.cts +585 -54
  59. package/dist/commands.d.mts +583 -51
  60. package/dist/commands.mjs +1084 -93
  61. package/dist/const.cjs +22 -0
  62. package/dist/const.d.cts +21 -0
  63. package/dist/const.d.mts +21 -0
  64. package/dist/const.mjs +20 -1
  65. package/dist/energy.cjs +24 -19
  66. package/dist/energy.d.cts +21 -15
  67. package/dist/energy.d.mts +21 -15
  68. package/dist/energy.mjs +24 -19
  69. package/dist/energyisland.cjs +38 -0
  70. package/dist/energyisland.mjs +37 -0
  71. package/dist/energyparsers.cjs +118 -0
  72. package/dist/energyparsers.d.cts +39 -0
  73. package/dist/energyparsers.d.mts +39 -0
  74. package/dist/energyparsers.mjs +116 -0
  75. package/dist/energyspecific.cjs +375 -17
  76. package/dist/energyspecific.d.cts +199 -19
  77. package/dist/energyspecific.d.mts +197 -16
  78. package/dist/energyspecific.mjs +370 -18
  79. package/dist/exceptions.cjs +3 -2
  80. package/dist/exceptions.d.cts +3 -2
  81. package/dist/exceptions.d.mts +3 -2
  82. package/dist/exceptions.mjs +3 -2
  83. package/dist/funnel.cjs +297 -0
  84. package/dist/funnel.d.cts +118 -0
  85. package/dist/funnel.d.mts +118 -0
  86. package/dist/funnel.mjs +292 -0
  87. package/dist/index.cjs +61 -0
  88. package/dist/index.d.cts +22 -8
  89. package/dist/index.d.mts +22 -8
  90. package/dist/index.mjs +19 -6
  91. package/dist/log.cjs +75 -0
  92. package/dist/log.d.cts +4 -0
  93. package/dist/log.d.mts +4 -0
  94. package/dist/log.mjs +71 -0
  95. package/dist/oauth.cjs +143 -0
  96. package/dist/oauth.d.cts +69 -0
  97. package/dist/oauth.d.mts +69 -0
  98. package/dist/oauth.mjs +143 -0
  99. package/dist/powerwall/client.cjs +818 -0
  100. package/dist/powerwall/client.d.cts +297 -0
  101. package/dist/powerwall/client.d.mts +297 -0
  102. package/dist/powerwall/client.mjs +807 -0
  103. package/dist/powerwall/components.cjs +188 -0
  104. package/dist/powerwall/components.d.cts +45 -0
  105. package/dist/powerwall/components.d.mts +45 -0
  106. package/dist/powerwall/components.mjs +182 -0
  107. package/dist/powerwall/energysite.cjs +216 -0
  108. package/dist/powerwall/energysite.d.cts +104 -0
  109. package/dist/powerwall/energysite.d.mts +104 -0
  110. package/dist/powerwall/energysite.mjs +214 -0
  111. package/dist/powerwall/exceptions.cjs +59 -0
  112. package/dist/powerwall/exceptions.d.cts +38 -0
  113. package/dist/powerwall/exceptions.d.mts +38 -0
  114. package/dist/powerwall/exceptions.mjs +51 -0
  115. package/dist/powerwall/gateway.cjs +102 -0
  116. package/dist/powerwall/gateway.d.cts +57 -0
  117. package/dist/powerwall/gateway.d.mts +57 -0
  118. package/dist/powerwall/gateway.mjs +101 -0
  119. package/dist/powerwall/merge.cjs +163 -0
  120. package/dist/powerwall/merge.d.cts +63 -0
  121. package/dist/powerwall/merge.d.mts +63 -0
  122. package/dist/powerwall/merge.mjs +159 -0
  123. package/dist/powerwall/models.d.cts +95 -0
  124. package/dist/powerwall/models.d.mts +95 -0
  125. package/dist/powerwall/queries.cjs +17 -0
  126. package/dist/powerwall/queries.mjs +10 -0
  127. package/dist/powerwall/transport.cjs +232 -0
  128. package/dist/powerwall/transport.mjs +227 -0
  129. package/dist/powerwall.cjs +47 -0
  130. package/dist/powerwall.d.cts +9 -0
  131. package/dist/powerwall.d.mts +9 -0
  132. package/dist/powerwall.mjs +8 -0
  133. package/dist/router/base.cjs +203 -0
  134. package/dist/router/base.d.cts +112 -0
  135. package/dist/router/base.d.mts +112 -0
  136. package/dist/router/base.mjs +200 -0
  137. package/dist/router/energysite.cjs +65 -0
  138. package/dist/router/energysite.d.cts +47 -0
  139. package/dist/router/energysite.d.mts +47 -0
  140. package/dist/router/energysite.mjs +60 -0
  141. package/dist/router.cjs +13 -0
  142. package/dist/router.d.cts +3 -1
  143. package/dist/router.d.mts +3 -1
  144. package/dist/router.mjs +3 -1
  145. package/dist/signing/crypto.cjs +28 -0
  146. package/dist/signing/crypto.mjs +26 -1
  147. package/dist/signing/energy.cjs +123 -0
  148. package/dist/signing/energy.d.cts +45 -0
  149. package/dist/signing/energy.d.mts +45 -0
  150. package/dist/signing/energy.mjs +120 -0
  151. package/dist/signing/jws.cjs +229 -0
  152. package/dist/signing/jws.d.cts +45 -0
  153. package/dist/signing/jws.d.mts +45 -0
  154. package/dist/signing/jws.mjs +211 -0
  155. package/dist/signing/keys.cjs +191 -0
  156. package/dist/signing/keys.d.cts +21 -0
  157. package/dist/signing/keys.d.mts +21 -0
  158. package/dist/signing/keys.mjs +182 -0
  159. package/dist/signing/session.cjs +104 -30
  160. package/dist/signing/session.d.cts +60 -23
  161. package/dist/signing/session.d.mts +60 -23
  162. package/dist/signing/session.mjs +105 -31
  163. package/dist/tariff.cjs +27 -0
  164. package/dist/tariff.d.cts +11 -0
  165. package/dist/tariff.d.mts +11 -0
  166. package/dist/tariff.mjs +27 -1
  167. package/dist/teslafleetapi.cjs +256 -55
  168. package/dist/teslafleetapi.d.cts +110 -14
  169. package/dist/teslafleetapi.d.mts +110 -14
  170. package/dist/teslafleetapi.mjs +258 -57
  171. package/dist/teslemetry.cjs +152 -5
  172. package/dist/teslemetry.d.cts +95 -5
  173. package/dist/teslemetry.d.mts +93 -3
  174. package/dist/teslemetry.mjs +151 -6
  175. package/dist/teslemetryenergy.cjs +287 -0
  176. package/dist/teslemetryenergy.d.cts +154 -0
  177. package/dist/teslemetryenergy.d.mts +154 -0
  178. package/dist/teslemetryenergy.mjs +282 -0
  179. package/dist/teslemetryvehicle.cjs +407 -0
  180. package/dist/teslemetryvehicle.d.cts +238 -0
  181. package/dist/teslemetryvehicle.d.mts +238 -0
  182. package/dist/teslemetryvehicle.mjs +405 -0
  183. package/dist/tessie.cjs +42 -5
  184. package/dist/tessie.d.cts +51 -5
  185. package/dist/tessie.d.mts +49 -3
  186. package/dist/tessie.mjs +42 -5
  187. package/dist/tessievehicle.cjs +514 -0
  188. package/dist/tessievehicle.d.cts +333 -0
  189. package/dist/tessievehicle.d.mts +333 -0
  190. package/dist/tessievehicle.mjs +513 -0
  191. package/dist/types/live_status.d.cts +16 -16
  192. package/dist/types/live_status.d.mts +16 -16
  193. package/dist/types/responses.d.cts +5 -1
  194. package/dist/types/responses.d.mts +5 -1
  195. package/dist/types/vehicle.d.cts +7 -1
  196. package/dist/types/vehicle.d.mts +7 -1
  197. package/dist/types.d.cts +3 -3
  198. package/dist/types.d.mts +3 -3
  199. package/dist/util.cjs +41 -0
  200. package/dist/util.d.cts +18 -0
  201. package/dist/util.d.mts +18 -0
  202. package/dist/util.mjs +40 -0
  203. package/dist/vehicle.cjs +79 -36
  204. package/dist/vehicle.d.cts +42 -26
  205. package/dist/vehicle.d.mts +42 -26
  206. package/dist/vehicle.mjs +80 -37
  207. package/dist/vehiclesigned.cjs +20 -5
  208. package/dist/vehiclesigned.d.cts +14 -5
  209. package/dist/vehiclesigned.d.mts +14 -5
  210. package/dist/vehiclesigned.mjs +20 -5
  211. package/dist/vehiclespecific.cjs +126 -15
  212. package/dist/vehiclespecific.d.cts +82 -18
  213. package/dist/vehiclespecific.d.mts +82 -18
  214. package/dist/vehiclespecific.mjs +126 -15
  215. package/package.json +56 -3
@@ -0,0 +1,177 @@
1
+ import { AsyncLock, withTimeout } from "./async.mjs";
2
+ import { APPEARANCE_UUID, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS, GENERIC_ACCESS_UUID, NAME_UUID, SERVICE_UUID, VEHICLE_NOTIFY_UUID, VEHICLE_WRITE_UUID, VERSION_UUID } from "./constants.mjs";
3
+ import { BleNotSubmittedError } from "./transport.mjs";
4
+ import { LinkClaims, compactUuid, quietly } from "./adapters/shared.mjs";
5
+ // src/bluetooth/adapters/noble.ts
6
+ function find(characteristics, uuid) {
7
+ return characteristics.find((characteristic) => compactUuid(characteristic.uuid) === compactUuid(uuid));
8
+ }
9
+ async function loadNoble(bindings) {
10
+ const specifier = "@stoprocent/noble/lib/resolve-bindings.js";
11
+ let module;
12
+ try {
13
+ module = await import(specifier);
14
+ } catch (cause) {
15
+ throw new Error("NobleBleTransport needs @stoprocent/noble, or a `noble` option.", { cause });
16
+ }
17
+ return module.default(bindings);
18
+ }
19
+ var NobleConnection = class {
20
+ peripheral;
21
+ handlers;
22
+ claims;
23
+ lost = false;
24
+ closed = false;
25
+ closing;
26
+ genericAccess;
27
+ characteristics = [];
28
+ onData = (data, isNotification) => {
29
+ if (isNotification !== false) this.handlers.onNotify(data);
30
+ };
31
+ onDisconnect = () => {
32
+ this.lost = true;
33
+ if (!this.closed) this.handlers.onDisconnect();
34
+ };
35
+ constructor(peripheral, handlers, claims) {
36
+ this.peripheral = peripheral;
37
+ this.handlers = handlers;
38
+ this.claims = claims;
39
+ }
40
+ get isConnected() {
41
+ return !this.lost && !this.closed && this.peripheral.state === "connected";
42
+ }
43
+ /** Read each time: noble can report a larger MTU after the link is up. */
44
+ get maxWriteSize() {
45
+ const { mtu } = this.peripheral;
46
+ return mtu && mtu > 3 ? mtu - 3 : 20;
47
+ }
48
+ /** Resolve the characteristics and turn notifications on. */
49
+ async subscribe() {
50
+ const { peripheral } = this;
51
+ const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync([compactUuid(SERVICE_UUID)], [
52
+ VEHICLE_WRITE_UUID,
53
+ VEHICLE_NOTIFY_UUID,
54
+ VERSION_UUID
55
+ ].map(compactUuid));
56
+ const notify = find(characteristics, VEHICLE_NOTIFY_UUID);
57
+ if (!notify || !find(characteristics, "00000212-b2d1-43f0-9b88-960cebf8b91e")) throw new Error("The vehicle's write or notify characteristic was not found.");
58
+ this.assertCurrent();
59
+ this.characteristics = characteristics;
60
+ notify.on("data", this.onData);
61
+ await notify.subscribeAsync();
62
+ }
63
+ /** A superseded attempt stops here, before it changes a link another attempt owns. */
64
+ assertCurrent() {
65
+ if (this.claims.superseded(this.peripheral.id, this)) throw new Error("A newer connect attempt took over this link.");
66
+ }
67
+ async write(data) {
68
+ const characteristic = find(this.characteristics, VEHICLE_WRITE_UUID);
69
+ if (!characteristic) throw new BleNotSubmittedError("The vehicle write characteristic was not found.");
70
+ if (!this.isConnected) throw new BleNotSubmittedError("The link is down.");
71
+ await characteristic.writeAsync(Buffer.from(data), false);
72
+ }
73
+ async read(characteristic) {
74
+ const target = characteristic === "version" ? find(this.characteristics, VERSION_UUID) : find(await this.discoverGenericAccess(), characteristic === "deviceName" ? NAME_UUID : APPEARANCE_UUID);
75
+ if (!target) throw new BleNotSubmittedError(`The ${characteristic} characteristic was not found.`);
76
+ return target.readAsync();
77
+ }
78
+ /**
79
+ * BlueZ and CoreBluetooth can hide this service. A failed lookup leaves a
80
+ * characteristic unresolved on a healthy link, so it must not read as a lost one.
81
+ */
82
+ discoverGenericAccess() {
83
+ this.genericAccess ??= this.peripheral.discoverSomeServicesAndCharacteristicsAsync([compactUuid(GENERIC_ACCESS_UUID)], [compactUuid(NAME_UUID), compactUuid(APPEARANCE_UUID)]).then(({ characteristics }) => characteristics).catch((cause) => {
84
+ this.genericAccess = void 0;
85
+ throw new BleNotSubmittedError("Generic Access could not be resolved.", { cause });
86
+ });
87
+ return this.genericAccess;
88
+ }
89
+ disconnect() {
90
+ this.closing ??= (async () => {
91
+ this.closed = true;
92
+ const { peripheral, claims } = this;
93
+ peripheral.removeListener("disconnect", this.onDisconnect);
94
+ find(this.characteristics, VEHICLE_NOTIFY_UUID)?.removeListener("data", this.onData);
95
+ const superseded = claims.superseded(peripheral.id, this);
96
+ claims.release(peripheral.id, this);
97
+ if (!superseded && peripheral.state !== "disconnected") await quietly(() => peripheral.disconnectAsync());
98
+ })();
99
+ return this.closing;
100
+ }
101
+ };
102
+ var NobleBleTransport = class {
103
+ options;
104
+ backend;
105
+ scans = new AsyncLock();
106
+ peripherals = /* @__PURE__ */ new Map();
107
+ claims = new LinkClaims();
108
+ constructor(options = {}) {
109
+ this.options = options;
110
+ }
111
+ noble() {
112
+ if (this.options.noble) return Promise.resolve(this.options.noble);
113
+ const fallback = process.platform === "linux" ? "dbus" : "default";
114
+ const loading = this.backend ??= loadNoble(this.options.bindings ?? fallback);
115
+ loading.catch(() => {
116
+ this.backend = void 0;
117
+ });
118
+ return loading;
119
+ }
120
+ async scan({ timeoutMs, matches }) {
121
+ const noble = await this.noble();
122
+ return this.listen(noble, timeoutMs, (device) => device.name !== void 0 && matches?.(device.name) === true);
123
+ }
124
+ /** Scan with no service filter until `done` accepts a device or the window ends. */
125
+ listen(noble, timeoutMs, done) {
126
+ return this.scans.run(async () => {
127
+ await noble.waitForPoweredOnAsync(timeoutMs);
128
+ const seen = /* @__PURE__ */ new Map();
129
+ let finish;
130
+ const finished = new Promise((resolve) => {
131
+ finish = resolve;
132
+ });
133
+ const onDiscover = (peripheral) => {
134
+ this.peripherals.set(peripheral.id, peripheral);
135
+ const device = {
136
+ id: peripheral.id,
137
+ name: peripheral.advertisement.localName ?? seen.get(peripheral.id)?.name,
138
+ rssi: peripheral.rssi,
139
+ connectable: peripheral.connectable
140
+ };
141
+ seen.set(peripheral.id, device);
142
+ if (done(device)) finish();
143
+ };
144
+ noble.on("discover", onDiscover);
145
+ const timer = setTimeout(finish, Math.max(0, timeoutMs - 250));
146
+ try {
147
+ await noble.startScanningAsync([], true);
148
+ await finished;
149
+ } finally {
150
+ clearTimeout(timer);
151
+ noble.removeListener("discover", onDiscover);
152
+ await quietly(() => noble.stopScanningAsync());
153
+ }
154
+ return [...seen.values()];
155
+ });
156
+ }
157
+ async connect(device, handlers) {
158
+ const noble = await this.noble();
159
+ if (!this.peripherals.has(device.id)) await this.listen(noble, DEFAULT_SCAN_TIMEOUT_MS, ({ id }) => id === device.id);
160
+ const peripheral = this.peripherals.get(device.id);
161
+ if (!peripheral) throw new Error("The device was not seen in a scan.");
162
+ await quietly(() => noble.stopScanningAsync());
163
+ const connection = new NobleConnection(peripheral, handlers, this.claims);
164
+ this.claims.claim(device.id, connection);
165
+ peripheral.once("disconnect", connection.onDisconnect);
166
+ try {
167
+ await peripheral.connectAsync();
168
+ connection.assertCurrent();
169
+ await withTimeout(() => connection.subscribe(), DEFAULT_CONNECT_TIMEOUT_MS);
170
+ } catch (error) {
171
+ await connection.disconnect();
172
+ throw error;
173
+ }
174
+ return connection;
175
+ }
176
+ };
177
+ export { NobleBleTransport };
@@ -0,0 +1,157 @@
1
+ const require_funnel = require("../funnel.cjs");
2
+ let _teslemetry_tesla_protocol_command_vcsec = require("@teslemetry/tesla-protocol/command/vcsec");
3
+ // src/bluetooth/streamglue.ts
4
+ const GEAR_STATES = /* @__PURE__ */ new Map([
5
+ [_teslemetry_tesla_protocol_command_vcsec.GearE.GEAR_PARK, "ShiftStateP"],
6
+ [_teslemetry_tesla_protocol_command_vcsec.GearE.GEAR_DRIVE, "ShiftStateD"],
7
+ [_teslemetry_tesla_protocol_command_vcsec.GearE.GEAR_REVERSE, "ShiftStateR"],
8
+ [_teslemetry_tesla_protocol_command_vcsec.GearE.GEAR_NEUTRAL, "ShiftStateN"]
9
+ ]);
10
+ const TONNEAU_POSITION_STATES = /* @__PURE__ */ new Map([
11
+ [_teslemetry_tesla_protocol_command_vcsec.ClosureStateE.CLOSURESTATE_CLOSED, "TonneauPositionStateClosed"],
12
+ [_teslemetry_tesla_protocol_command_vcsec.ClosureStateE.CLOSURESTATE_OPEN, "TonneauPositionStateFullyOpen"],
13
+ [_teslemetry_tesla_protocol_command_vcsec.ClosureStateE.CLOSURESTATE_AJAR, "TonneauPositionStatePartiallyOpen"]
14
+ ]);
15
+ /** A closure state's name, or the bare number for one this build has no name for. */
16
+ function closureName(state) {
17
+ const name = (0, _teslemetry_tesla_protocol_command_vcsec.closureStateEToJSON)(state);
18
+ return name === "UNRECOGNIZED" ? String(state) : name;
19
+ }
20
+ /**
21
+ * Feeds a vehicle's broadcasts into a stream-shaped sink, so a Bluetooth reading
22
+ * reaches the listeners a native stream event does. There is no source ranking:
23
+ * every call reaches them in arrival order.
24
+ */
25
+ var BleBroadcastStreamGlue = class {
26
+ #sink;
27
+ #unsubscribes;
28
+ constructor(vehicle, sink) {
29
+ this.#sink = sink;
30
+ this.#unsubscribes = [
31
+ vehicle.listen_vehicle_lock_state((state) => this.#onLockState(state)),
32
+ vehicle.listen_charge_port((state) => this.#onChargePort(state)),
33
+ vehicle.listen_closure_statuses((closures) => this.#onClosures(closures)),
34
+ vehicle.listen_gear((gear) => this.#onGear(gear)),
35
+ vehicle.listen_tonneau((state) => this.#onTonneau(state)),
36
+ vehicle.listen_tonneau_percent_open((percent) => this.#sink.ingest({ TonneauOpenPercent: percent }, { source: "bluetooth" }))
37
+ ];
38
+ }
39
+ /** Stop listening. Safe to call twice. */
40
+ stop() {
41
+ for (const unsubscribe of this.#unsubscribes) unsubscribe();
42
+ this.#unsubscribes = [];
43
+ }
44
+ #onLockState(state) {
45
+ const locked = require_funnel.LOCK_STATES.get(state);
46
+ if (locked !== void 0) this.#sink.ingest({ Locked: locked }, {
47
+ source: "bluetooth",
48
+ raw: (0, _teslemetry_tesla_protocol_command_vcsec.vehicleLockStateEToJSON)(state)
49
+ });
50
+ }
51
+ #onChargePort(state) {
52
+ const open = require_funnel.CLOSURE_STATES.get(state);
53
+ if (open !== void 0) this.#sink.ingest({ ChargePortDoorOpen: open }, {
54
+ source: "bluetooth",
55
+ raw: (0, _teslemetry_tesla_protocol_command_vcsec.closureStateEToJSON)(state)
56
+ });
57
+ }
58
+ /**
59
+ * Every door listener fires on any `DoorState` event and reads its own key, so
60
+ * a leaf sent alone would reach the other five as missing. All six go in one
61
+ * event, as a native event's do, with a leaf in an unmapped state left out.
62
+ */
63
+ #onClosures(closures) {
64
+ const raw = {
65
+ DriverFront: closures.frontDriverDoor,
66
+ PassengerFront: closures.frontPassengerDoor,
67
+ DriverRear: closures.rearDriverDoor,
68
+ PassengerRear: closures.rearPassengerDoor,
69
+ TrunkFront: closures.frontTrunk,
70
+ TrunkRear: closures.rearTrunk
71
+ };
72
+ const leaves = Object.entries(raw);
73
+ const doorState = leaves.flatMap(([key, state]) => {
74
+ const open = require_funnel.CLOSURE_STATES.get(state);
75
+ return open === void 0 ? [] : [[key, open]];
76
+ });
77
+ if (doorState.length > 0) this.#sink.ingest({ DoorState: Object.fromEntries(doorState) }, {
78
+ source: "bluetooth",
79
+ raw: Object.fromEntries(leaves.map(([key, state]) => [key, closureName(state)]))
80
+ });
81
+ }
82
+ #onGear(gear) {
83
+ const state = GEAR_STATES.get(gear);
84
+ if (state !== void 0) this.#sink.ingest({ Gear: state }, {
85
+ source: "bluetooth",
86
+ raw: (0, _teslemetry_tesla_protocol_command_vcsec.gearEToJSON)(gear)
87
+ });
88
+ }
89
+ #onTonneau(state) {
90
+ const position = TONNEAU_POSITION_STATES.get(state);
91
+ if (position !== void 0) this.#sink.ingest({ TonneauPosition: position }, {
92
+ source: "bluetooth",
93
+ raw: (0, _teslemetry_tesla_protocol_command_vcsec.closureStateEToJSON)(state)
94
+ });
95
+ }
96
+ };
97
+ /** The map and listener behind each field a broadcast carries. */
98
+ const BROADCAST_FIELDS = /* @__PURE__ */ new Map([
99
+ [require_funnel.FieldPath.LOCKED, {
100
+ states: require_funnel.LOCK_STATES,
101
+ listen: (vehicle, callback) => vehicle.listen_vehicle_lock_state(callback)
102
+ }],
103
+ [require_funnel.FieldPath.CHARGE_PORT_DOOR_OPEN, {
104
+ states: require_funnel.CLOSURE_STATES,
105
+ listen: (vehicle, callback) => vehicle.listen_charge_port(callback)
106
+ }],
107
+ [require_funnel.FieldPath.DOOR_STATE_TRUNK_FRONT, {
108
+ states: require_funnel.CLOSURE_STATES,
109
+ listen: (vehicle, callback) => vehicle.listen_front_trunk(callback)
110
+ }]
111
+ ]);
112
+ /**
113
+ * Publishes VCSEC status broadcasts to an `ObservationFunnel`. Registration only:
114
+ * a dropped session ends the broadcasts and says nothing about the fields, so it
115
+ * publishes nothing.
116
+ */
117
+ var BleBroadcastPublisher = class {
118
+ paths = new Set(BROADCAST_FIELDS.keys());
119
+ #vehicle;
120
+ #clock;
121
+ #subscriptions = /* @__PURE__ */ new Map();
122
+ #sink = null;
123
+ constructor(vehicle, options = {}) {
124
+ this.#vehicle = vehicle;
125
+ this.#clock = options.clock ?? (() => performance.now() / 1e3);
126
+ }
127
+ attach(sink) {
128
+ this.#sink = sink;
129
+ return () => {
130
+ this.release(new Set(this.#subscriptions.keys()));
131
+ this.#sink = null;
132
+ };
133
+ }
134
+ request(paths) {
135
+ for (const path of paths) {
136
+ const field = BROADCAST_FIELDS.get(path);
137
+ if (field && !this.#subscriptions.has(path)) this.#subscriptions.set(path, field.listen(this.#vehicle, (state) => {
138
+ const value = field.states.get(state);
139
+ if (value !== void 0) this.#sink?.publish({
140
+ path,
141
+ value,
142
+ observed_at: this.#clock()
143
+ });
144
+ }));
145
+ }
146
+ }
147
+ release(paths) {
148
+ for (const path of paths) {
149
+ this.#subscriptions.get(path)?.();
150
+ this.#subscriptions.delete(path);
151
+ }
152
+ }
153
+ };
154
+ exports.BleBroadcastPublisher = BleBroadcastPublisher;
155
+ exports.BleBroadcastStreamGlue = BleBroadcastStreamGlue;
156
+ exports.GEAR_STATES = GEAR_STATES;
157
+ exports.TONNEAU_POSITION_STATES = TONNEAU_POSITION_STATES;
@@ -0,0 +1,37 @@
1
+ import { Clock, FieldPath, ObservationSink, Publisher, Unsubscribe } from "../funnel.cjs";
2
+ import { BroadcastListeners } from "./broadcast.cjs";
3
+ /**
4
+ * What the glue feeds. Structurally a Teslemetry stream's `ingest`: no stream
5
+ * package is imported here, so a sink can be anything with this method.
6
+ */
7
+ export interface StreamSink {
8
+ ingest(data: Record<string, unknown>, metadata?: Record<string, unknown>): unknown;
9
+ }
10
+ export declare const GEAR_STATES: ReadonlyMap<number, string>;
11
+ export declare const TONNEAU_POSITION_STATES: ReadonlyMap<number, string>;
12
+ /**
13
+ * Feeds a vehicle's broadcasts into a stream-shaped sink, so a Bluetooth reading
14
+ * reaches the listeners a native stream event does. There is no source ranking:
15
+ * every call reaches them in arrival order.
16
+ */
17
+ export declare class BleBroadcastStreamGlue {
18
+ #private;
19
+ constructor(vehicle: BroadcastListeners, sink: StreamSink);
20
+ /** Stop listening. Safe to call twice. */
21
+ stop(): void;
22
+ }
23
+ /**
24
+ * Publishes VCSEC status broadcasts to an `ObservationFunnel`. Registration only:
25
+ * a dropped session ends the broadcasts and says nothing about the fields, so it
26
+ * publishes nothing.
27
+ */
28
+ export declare class BleBroadcastPublisher implements Publisher {
29
+ #private;
30
+ readonly paths: ReadonlySet<FieldPath>;
31
+ constructor(vehicle: BroadcastListeners, options?: {
32
+ clock?: Clock;
33
+ });
34
+ attach(sink: ObservationSink): Unsubscribe;
35
+ request(paths: ReadonlySet<FieldPath>): void;
36
+ release(paths: ReadonlySet<FieldPath>): void;
37
+ }
@@ -0,0 +1,37 @@
1
+ import { Clock, FieldPath, ObservationSink, Publisher, Unsubscribe } from "../funnel.mjs";
2
+ import { BroadcastListeners } from "./broadcast.mjs";
3
+ /**
4
+ * What the glue feeds. Structurally a Teslemetry stream's `ingest`: no stream
5
+ * package is imported here, so a sink can be anything with this method.
6
+ */
7
+ export interface StreamSink {
8
+ ingest(data: Record<string, unknown>, metadata?: Record<string, unknown>): unknown;
9
+ }
10
+ export declare const GEAR_STATES: ReadonlyMap<number, string>;
11
+ export declare const TONNEAU_POSITION_STATES: ReadonlyMap<number, string>;
12
+ /**
13
+ * Feeds a vehicle's broadcasts into a stream-shaped sink, so a Bluetooth reading
14
+ * reaches the listeners a native stream event does. There is no source ranking:
15
+ * every call reaches them in arrival order.
16
+ */
17
+ export declare class BleBroadcastStreamGlue {
18
+ #private;
19
+ constructor(vehicle: BroadcastListeners, sink: StreamSink);
20
+ /** Stop listening. Safe to call twice. */
21
+ stop(): void;
22
+ }
23
+ /**
24
+ * Publishes VCSEC status broadcasts to an `ObservationFunnel`. Registration only:
25
+ * a dropped session ends the broadcasts and says nothing about the fields, so it
26
+ * publishes nothing.
27
+ */
28
+ export declare class BleBroadcastPublisher implements Publisher {
29
+ #private;
30
+ readonly paths: ReadonlySet<FieldPath>;
31
+ constructor(vehicle: BroadcastListeners, options?: {
32
+ clock?: Clock;
33
+ });
34
+ attach(sink: ObservationSink): Unsubscribe;
35
+ request(paths: ReadonlySet<FieldPath>): void;
36
+ release(paths: ReadonlySet<FieldPath>): void;
37
+ }
@@ -0,0 +1,154 @@
1
+ import { CLOSURE_STATES, FieldPath, LOCK_STATES } from "../funnel.mjs";
2
+ import { ClosureStateE, GearE, closureStateEToJSON, gearEToJSON, vehicleLockStateEToJSON } from "@teslemetry/tesla-protocol/command/vcsec";
3
+ // src/bluetooth/streamglue.ts
4
+ const GEAR_STATES = /* @__PURE__ */ new Map([
5
+ [GearE.GEAR_PARK, "ShiftStateP"],
6
+ [GearE.GEAR_DRIVE, "ShiftStateD"],
7
+ [GearE.GEAR_REVERSE, "ShiftStateR"],
8
+ [GearE.GEAR_NEUTRAL, "ShiftStateN"]
9
+ ]);
10
+ const TONNEAU_POSITION_STATES = /* @__PURE__ */ new Map([
11
+ [ClosureStateE.CLOSURESTATE_CLOSED, "TonneauPositionStateClosed"],
12
+ [ClosureStateE.CLOSURESTATE_OPEN, "TonneauPositionStateFullyOpen"],
13
+ [ClosureStateE.CLOSURESTATE_AJAR, "TonneauPositionStatePartiallyOpen"]
14
+ ]);
15
+ /** A closure state's name, or the bare number for one this build has no name for. */
16
+ function closureName(state) {
17
+ const name = closureStateEToJSON(state);
18
+ return name === "UNRECOGNIZED" ? String(state) : name;
19
+ }
20
+ /**
21
+ * Feeds a vehicle's broadcasts into a stream-shaped sink, so a Bluetooth reading
22
+ * reaches the listeners a native stream event does. There is no source ranking:
23
+ * every call reaches them in arrival order.
24
+ */
25
+ var BleBroadcastStreamGlue = class {
26
+ #sink;
27
+ #unsubscribes;
28
+ constructor(vehicle, sink) {
29
+ this.#sink = sink;
30
+ this.#unsubscribes = [
31
+ vehicle.listen_vehicle_lock_state((state) => this.#onLockState(state)),
32
+ vehicle.listen_charge_port((state) => this.#onChargePort(state)),
33
+ vehicle.listen_closure_statuses((closures) => this.#onClosures(closures)),
34
+ vehicle.listen_gear((gear) => this.#onGear(gear)),
35
+ vehicle.listen_tonneau((state) => this.#onTonneau(state)),
36
+ vehicle.listen_tonneau_percent_open((percent) => this.#sink.ingest({ TonneauOpenPercent: percent }, { source: "bluetooth" }))
37
+ ];
38
+ }
39
+ /** Stop listening. Safe to call twice. */
40
+ stop() {
41
+ for (const unsubscribe of this.#unsubscribes) unsubscribe();
42
+ this.#unsubscribes = [];
43
+ }
44
+ #onLockState(state) {
45
+ const locked = LOCK_STATES.get(state);
46
+ if (locked !== void 0) this.#sink.ingest({ Locked: locked }, {
47
+ source: "bluetooth",
48
+ raw: vehicleLockStateEToJSON(state)
49
+ });
50
+ }
51
+ #onChargePort(state) {
52
+ const open = CLOSURE_STATES.get(state);
53
+ if (open !== void 0) this.#sink.ingest({ ChargePortDoorOpen: open }, {
54
+ source: "bluetooth",
55
+ raw: closureStateEToJSON(state)
56
+ });
57
+ }
58
+ /**
59
+ * Every door listener fires on any `DoorState` event and reads its own key, so
60
+ * a leaf sent alone would reach the other five as missing. All six go in one
61
+ * event, as a native event's do, with a leaf in an unmapped state left out.
62
+ */
63
+ #onClosures(closures) {
64
+ const raw = {
65
+ DriverFront: closures.frontDriverDoor,
66
+ PassengerFront: closures.frontPassengerDoor,
67
+ DriverRear: closures.rearDriverDoor,
68
+ PassengerRear: closures.rearPassengerDoor,
69
+ TrunkFront: closures.frontTrunk,
70
+ TrunkRear: closures.rearTrunk
71
+ };
72
+ const leaves = Object.entries(raw);
73
+ const doorState = leaves.flatMap(([key, state]) => {
74
+ const open = CLOSURE_STATES.get(state);
75
+ return open === void 0 ? [] : [[key, open]];
76
+ });
77
+ if (doorState.length > 0) this.#sink.ingest({ DoorState: Object.fromEntries(doorState) }, {
78
+ source: "bluetooth",
79
+ raw: Object.fromEntries(leaves.map(([key, state]) => [key, closureName(state)]))
80
+ });
81
+ }
82
+ #onGear(gear) {
83
+ const state = GEAR_STATES.get(gear);
84
+ if (state !== void 0) this.#sink.ingest({ Gear: state }, {
85
+ source: "bluetooth",
86
+ raw: gearEToJSON(gear)
87
+ });
88
+ }
89
+ #onTonneau(state) {
90
+ const position = TONNEAU_POSITION_STATES.get(state);
91
+ if (position !== void 0) this.#sink.ingest({ TonneauPosition: position }, {
92
+ source: "bluetooth",
93
+ raw: closureStateEToJSON(state)
94
+ });
95
+ }
96
+ };
97
+ /** The map and listener behind each field a broadcast carries. */
98
+ const BROADCAST_FIELDS = /* @__PURE__ */ new Map([
99
+ [FieldPath.LOCKED, {
100
+ states: LOCK_STATES,
101
+ listen: (vehicle, callback) => vehicle.listen_vehicle_lock_state(callback)
102
+ }],
103
+ [FieldPath.CHARGE_PORT_DOOR_OPEN, {
104
+ states: CLOSURE_STATES,
105
+ listen: (vehicle, callback) => vehicle.listen_charge_port(callback)
106
+ }],
107
+ [FieldPath.DOOR_STATE_TRUNK_FRONT, {
108
+ states: CLOSURE_STATES,
109
+ listen: (vehicle, callback) => vehicle.listen_front_trunk(callback)
110
+ }]
111
+ ]);
112
+ /**
113
+ * Publishes VCSEC status broadcasts to an `ObservationFunnel`. Registration only:
114
+ * a dropped session ends the broadcasts and says nothing about the fields, so it
115
+ * publishes nothing.
116
+ */
117
+ var BleBroadcastPublisher = class {
118
+ paths = new Set(BROADCAST_FIELDS.keys());
119
+ #vehicle;
120
+ #clock;
121
+ #subscriptions = /* @__PURE__ */ new Map();
122
+ #sink = null;
123
+ constructor(vehicle, options = {}) {
124
+ this.#vehicle = vehicle;
125
+ this.#clock = options.clock ?? (() => performance.now() / 1e3);
126
+ }
127
+ attach(sink) {
128
+ this.#sink = sink;
129
+ return () => {
130
+ this.release(new Set(this.#subscriptions.keys()));
131
+ this.#sink = null;
132
+ };
133
+ }
134
+ request(paths) {
135
+ for (const path of paths) {
136
+ const field = BROADCAST_FIELDS.get(path);
137
+ if (field && !this.#subscriptions.has(path)) this.#subscriptions.set(path, field.listen(this.#vehicle, (state) => {
138
+ const value = field.states.get(state);
139
+ if (value !== void 0) this.#sink?.publish({
140
+ path,
141
+ value,
142
+ observed_at: this.#clock()
143
+ });
144
+ }));
145
+ }
146
+ }
147
+ release(paths) {
148
+ for (const path of paths) {
149
+ this.#subscriptions.get(path)?.();
150
+ this.#subscriptions.delete(path);
151
+ }
152
+ }
153
+ };
154
+ export { BleBroadcastPublisher, BleBroadcastStreamGlue, GEAR_STATES, TONNEAU_POSITION_STATES };
@@ -0,0 +1,67 @@
1
+ const require_exceptions = require("../exceptions.cjs");
2
+ const require_teslafleetapi = require("../teslafleetapi.cjs");
3
+ const require_vehicle = require("../vehicle.cjs");
4
+ const require_names = require("./names.cjs");
5
+ const require_vehiclebluetooth = require("./vehiclebluetooth.cjs");
6
+ // src/bluetooth/teslabluetooth.ts
7
+ /**
8
+ * The vehicles of a `TeslaBluetooth`. `createBluetooth` lives here, not on the
9
+ * root `Vehicle`, so the root entry never loads Bluetooth code.
10
+ */
11
+ var VehiclesBluetooth = class extends require_vehicle.default {
12
+ /** Class `createBluetooth` instantiates; a subclass overrides it to supply its own vehicle type. */
13
+ static Bluetooth = require_vehiclebluetooth.default;
14
+ /** Same as `createBluetooth`, under Python's other name. */
15
+ create(vin, options) {
16
+ return this.createBluetooth(vin, options);
17
+ }
18
+ /** Creates a vehicle reached over Bluetooth through `options.transport`. */
19
+ createBluetooth(vin, options) {
20
+ const vehicle = new this.constructor.Bluetooth(this, vin, options);
21
+ this.byVin.set(vin, vehicle);
22
+ return vehicle;
23
+ }
24
+ };
25
+ /**
26
+ * A client with no cloud behind it, for vehicles reached only over Bluetooth.
27
+ * Every REST path raises `BluetoothCloudUnavailableError` before any I/O, so a
28
+ * REST method a vehicle inherits fails clearly, sends nothing, and is safe for
29
+ * a router to fail over on. A cloud client that wants the same key on both
30
+ * paths uses `new VehicleBluetooth(client.vehicles, vin, options)` instead.
31
+ */
32
+ var TeslaBluetooth = class extends require_teslafleetapi.default {
33
+ static Vehicles = VehiclesBluetooth;
34
+ _transport_name = "bluetooth";
35
+ constructor(options = {}) {
36
+ super({
37
+ server: "bluetooth://local",
38
+ chargingScope: false,
39
+ energyScope: false,
40
+ partnerScope: false,
41
+ userScope: false,
42
+ debug: options.debug,
43
+ logger: options.logger
44
+ });
45
+ this.private_key = options.privateKey ?? null;
46
+ }
47
+ async _request() {
48
+ throw new require_exceptions.BluetoothCloudUnavailableError();
49
+ }
50
+ async _fetch() {
51
+ throw new require_exceptions.BluetoothCloudUnavailableError();
52
+ }
53
+ /** Whether a BLE device name is a valid Tesla vehicle's. */
54
+ valid_name(name) {
55
+ return require_names.valid_name(name);
56
+ }
57
+ /** The BLE device name of a vehicle. */
58
+ get_name(vin) {
59
+ return require_names.get_name(vin);
60
+ }
61
+ /** Whether an advertised name belongs to this VIN, with any valid suffix. */
62
+ name_matches_vin(advertised, vin) {
63
+ return require_names.name_matches_vin(advertised, vin);
64
+ }
65
+ };
66
+ exports.VehiclesBluetooth = VehiclesBluetooth;
67
+ exports.default = TeslaBluetooth;
@@ -0,0 +1,41 @@
1
+ import { Logger } from "../log.cjs";
2
+ import TeslaFleetApi from "../teslafleetapi.cjs";
3
+ import Vehicle from "../vehicle.cjs";
4
+ import VehicleBluetooth, { VehicleBluetoothOptions } from "./vehiclebluetooth.cjs";
5
+ /**
6
+ * The vehicles of a `TeslaBluetooth`. `createBluetooth` lives here, not on the
7
+ * root `Vehicle`, so the root entry never loads Bluetooth code.
8
+ */
9
+ export declare class VehiclesBluetooth extends Vehicle {
10
+ /** Class `createBluetooth` instantiates; a subclass overrides it to supply its own vehicle type. */
11
+ static Bluetooth: new (parent: Vehicle, vin: string, options: VehicleBluetoothOptions) => VehicleBluetooth;
12
+ /** Same as `createBluetooth`, under Python's other name. */
13
+ create(vin: string, options: VehicleBluetoothOptions): VehicleBluetooth;
14
+ /** Creates a vehicle reached over Bluetooth through `options.transport`. */
15
+ createBluetooth(vin: string, options: VehicleBluetoothOptions): VehicleBluetooth;
16
+ }
17
+ /**
18
+ * A client with no cloud behind it, for vehicles reached only over Bluetooth.
19
+ * Every REST path raises `BluetoothCloudUnavailableError` before any I/O, so a
20
+ * REST method a vehicle inherits fails clearly, sends nothing, and is safe for
21
+ * a router to fail over on. A cloud client that wants the same key on both
22
+ * paths uses `new VehicleBluetooth(client.vehicles, vin, options)` instead.
23
+ */
24
+ export default class TeslaBluetooth extends TeslaFleetApi {
25
+ static Vehicles: new (parent: TeslaFleetApi) => Vehicle;
26
+ vehicles: VehiclesBluetooth;
27
+ readonly _transport_name: string;
28
+ constructor(options?: {
29
+ privateKey?: Buffer;
30
+ debug?: boolean;
31
+ logger?: Logger;
32
+ });
33
+ _request(): Promise<never>;
34
+ protected _fetch(): Promise<never>;
35
+ /** Whether a BLE device name is a valid Tesla vehicle's. */
36
+ valid_name(name: string): boolean;
37
+ /** The BLE device name of a vehicle. */
38
+ get_name(vin: string): string;
39
+ /** Whether an advertised name belongs to this VIN, with any valid suffix. */
40
+ name_matches_vin(advertised: string, vin: string): boolean;
41
+ }