tesla-fleet-api 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +284 -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 +99 -0
  14. package/dist/bluetooth/constants.d.cts +70 -0
  15. package/dist/bluetooth/constants.d.mts +70 -0
  16. package/dist/bluetooth/constants.mjs +72 -0
  17. package/dist/bluetooth/esphome.cjs +292 -0
  18. package/dist/bluetooth/esphome.d.cts +83 -0
  19. package/dist/bluetooth/esphome.d.mts +83 -0
  20. package/dist/bluetooth/esphome.mjs +291 -0
  21. package/dist/bluetooth/framing.cjs +125 -0
  22. package/dist/bluetooth/framing.d.cts +45 -0
  23. package/dist/bluetooth/framing.d.mts +45 -0
  24. package/dist/bluetooth/framing.mjs +122 -0
  25. package/dist/bluetooth/homey.cjs +155 -0
  26. package/dist/bluetooth/homey.d.cts +75 -0
  27. package/dist/bluetooth/homey.d.mts +75 -0
  28. package/dist/bluetooth/homey.mjs +150 -0
  29. package/dist/bluetooth/names.cjs +24 -0
  30. package/dist/bluetooth/names.d.cts +10 -0
  31. package/dist/bluetooth/names.d.mts +10 -0
  32. package/dist/bluetooth/names.mjs +20 -0
  33. package/dist/bluetooth/noble.cjs +178 -0
  34. package/dist/bluetooth/noble.d.cts +58 -0
  35. package/dist/bluetooth/noble.d.mts +58 -0
  36. package/dist/bluetooth/noble.mjs +177 -0
  37. package/dist/bluetooth/streamglue.cjs +157 -0
  38. package/dist/bluetooth/streamglue.d.cts +37 -0
  39. package/dist/bluetooth/streamglue.d.mts +37 -0
  40. package/dist/bluetooth/streamglue.mjs +154 -0
  41. package/dist/bluetooth/teslabluetooth.cjs +67 -0
  42. package/dist/bluetooth/teslabluetooth.d.cts +41 -0
  43. package/dist/bluetooth/teslabluetooth.d.mts +41 -0
  44. package/dist/bluetooth/teslabluetooth.mjs +66 -0
  45. package/dist/bluetooth/transport.cjs +10 -0
  46. package/dist/bluetooth/transport.d.cts +95 -0
  47. package/dist/bluetooth/transport.d.mts +95 -0
  48. package/dist/bluetooth/transport.mjs +10 -0
  49. package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
  50. package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
  51. package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
  52. package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
  53. package/dist/bluetooth/verify.cjs +93 -0
  54. package/dist/bluetooth/verify.d.cts +35 -0
  55. package/dist/bluetooth/verify.d.mts +35 -0
  56. package/dist/bluetooth/verify.mjs +91 -0
  57. package/dist/bluetooth.cjs +53 -0
  58. package/dist/bluetooth.d.cts +10 -0
  59. package/dist/bluetooth.d.mts +10 -0
  60. package/dist/bluetooth.mjs +9 -0
  61. package/dist/commands.cjs +1080 -85
  62. package/dist/commands.d.cts +585 -54
  63. package/dist/commands.d.mts +583 -51
  64. package/dist/commands.mjs +1084 -93
  65. package/dist/const.cjs +22 -0
  66. package/dist/const.d.cts +21 -0
  67. package/dist/const.d.mts +21 -0
  68. package/dist/const.mjs +20 -1
  69. package/dist/energy.cjs +24 -19
  70. package/dist/energy.d.cts +21 -15
  71. package/dist/energy.d.mts +21 -15
  72. package/dist/energy.mjs +24 -19
  73. package/dist/energyisland.cjs +38 -0
  74. package/dist/energyisland.mjs +37 -0
  75. package/dist/energyparsers.cjs +118 -0
  76. package/dist/energyparsers.d.cts +39 -0
  77. package/dist/energyparsers.d.mts +39 -0
  78. package/dist/energyparsers.mjs +116 -0
  79. package/dist/energyspecific.cjs +375 -17
  80. package/dist/energyspecific.d.cts +199 -19
  81. package/dist/energyspecific.d.mts +197 -16
  82. package/dist/energyspecific.mjs +370 -18
  83. package/dist/exceptions.cjs +3 -2
  84. package/dist/exceptions.d.cts +3 -2
  85. package/dist/exceptions.d.mts +3 -2
  86. package/dist/exceptions.mjs +3 -2
  87. package/dist/funnel.cjs +297 -0
  88. package/dist/funnel.d.cts +118 -0
  89. package/dist/funnel.d.mts +118 -0
  90. package/dist/funnel.mjs +292 -0
  91. package/dist/index.cjs +61 -0
  92. package/dist/index.d.cts +22 -8
  93. package/dist/index.d.mts +22 -8
  94. package/dist/index.mjs +19 -6
  95. package/dist/log.cjs +75 -0
  96. package/dist/log.d.cts +4 -0
  97. package/dist/log.d.mts +4 -0
  98. package/dist/log.mjs +71 -0
  99. package/dist/oauth.cjs +143 -0
  100. package/dist/oauth.d.cts +69 -0
  101. package/dist/oauth.d.mts +69 -0
  102. package/dist/oauth.mjs +143 -0
  103. package/dist/powerwall/client.cjs +818 -0
  104. package/dist/powerwall/client.d.cts +297 -0
  105. package/dist/powerwall/client.d.mts +297 -0
  106. package/dist/powerwall/client.mjs +807 -0
  107. package/dist/powerwall/components.cjs +188 -0
  108. package/dist/powerwall/components.d.cts +45 -0
  109. package/dist/powerwall/components.d.mts +45 -0
  110. package/dist/powerwall/components.mjs +182 -0
  111. package/dist/powerwall/energysite.cjs +216 -0
  112. package/dist/powerwall/energysite.d.cts +104 -0
  113. package/dist/powerwall/energysite.d.mts +104 -0
  114. package/dist/powerwall/energysite.mjs +214 -0
  115. package/dist/powerwall/exceptions.cjs +59 -0
  116. package/dist/powerwall/exceptions.d.cts +38 -0
  117. package/dist/powerwall/exceptions.d.mts +38 -0
  118. package/dist/powerwall/exceptions.mjs +51 -0
  119. package/dist/powerwall/gateway.cjs +102 -0
  120. package/dist/powerwall/gateway.d.cts +57 -0
  121. package/dist/powerwall/gateway.d.mts +57 -0
  122. package/dist/powerwall/gateway.mjs +101 -0
  123. package/dist/powerwall/merge.cjs +163 -0
  124. package/dist/powerwall/merge.d.cts +63 -0
  125. package/dist/powerwall/merge.d.mts +63 -0
  126. package/dist/powerwall/merge.mjs +159 -0
  127. package/dist/powerwall/models.d.cts +95 -0
  128. package/dist/powerwall/models.d.mts +95 -0
  129. package/dist/powerwall/queries.cjs +17 -0
  130. package/dist/powerwall/queries.mjs +10 -0
  131. package/dist/powerwall/transport.cjs +232 -0
  132. package/dist/powerwall/transport.mjs +227 -0
  133. package/dist/powerwall.cjs +47 -0
  134. package/dist/powerwall.d.cts +9 -0
  135. package/dist/powerwall.d.mts +9 -0
  136. package/dist/powerwall.mjs +8 -0
  137. package/dist/router/base.cjs +203 -0
  138. package/dist/router/base.d.cts +112 -0
  139. package/dist/router/base.d.mts +112 -0
  140. package/dist/router/base.mjs +200 -0
  141. package/dist/router/energysite.cjs +65 -0
  142. package/dist/router/energysite.d.cts +47 -0
  143. package/dist/router/energysite.d.mts +47 -0
  144. package/dist/router/energysite.mjs +60 -0
  145. package/dist/router.cjs +13 -0
  146. package/dist/router.d.cts +3 -1
  147. package/dist/router.d.mts +3 -1
  148. package/dist/router.mjs +3 -1
  149. package/dist/signing/crypto.cjs +28 -0
  150. package/dist/signing/crypto.mjs +26 -1
  151. package/dist/signing/energy.cjs +123 -0
  152. package/dist/signing/energy.d.cts +45 -0
  153. package/dist/signing/energy.d.mts +45 -0
  154. package/dist/signing/energy.mjs +120 -0
  155. package/dist/signing/jws.cjs +229 -0
  156. package/dist/signing/jws.d.cts +45 -0
  157. package/dist/signing/jws.d.mts +45 -0
  158. package/dist/signing/jws.mjs +211 -0
  159. package/dist/signing/keys.cjs +191 -0
  160. package/dist/signing/keys.d.cts +21 -0
  161. package/dist/signing/keys.d.mts +21 -0
  162. package/dist/signing/keys.mjs +182 -0
  163. package/dist/signing/session.cjs +104 -30
  164. package/dist/signing/session.d.cts +60 -23
  165. package/dist/signing/session.d.mts +60 -23
  166. package/dist/signing/session.mjs +105 -31
  167. package/dist/tariff.cjs +27 -0
  168. package/dist/tariff.d.cts +11 -0
  169. package/dist/tariff.d.mts +11 -0
  170. package/dist/tariff.mjs +27 -1
  171. package/dist/teslafleetapi.cjs +256 -55
  172. package/dist/teslafleetapi.d.cts +110 -14
  173. package/dist/teslafleetapi.d.mts +110 -14
  174. package/dist/teslafleetapi.mjs +258 -57
  175. package/dist/teslemetry.cjs +152 -5
  176. package/dist/teslemetry.d.cts +95 -5
  177. package/dist/teslemetry.d.mts +93 -3
  178. package/dist/teslemetry.mjs +151 -6
  179. package/dist/teslemetryenergy.cjs +287 -0
  180. package/dist/teslemetryenergy.d.cts +154 -0
  181. package/dist/teslemetryenergy.d.mts +154 -0
  182. package/dist/teslemetryenergy.mjs +282 -0
  183. package/dist/teslemetryvehicle.cjs +407 -0
  184. package/dist/teslemetryvehicle.d.cts +238 -0
  185. package/dist/teslemetryvehicle.d.mts +238 -0
  186. package/dist/teslemetryvehicle.mjs +405 -0
  187. package/dist/tessie.cjs +42 -5
  188. package/dist/tessie.d.cts +51 -5
  189. package/dist/tessie.d.mts +49 -3
  190. package/dist/tessie.mjs +42 -5
  191. package/dist/tessievehicle.cjs +514 -0
  192. package/dist/tessievehicle.d.cts +333 -0
  193. package/dist/tessievehicle.d.mts +333 -0
  194. package/dist/tessievehicle.mjs +513 -0
  195. package/dist/types/live_status.d.cts +16 -16
  196. package/dist/types/live_status.d.mts +16 -16
  197. package/dist/types/responses.d.cts +5 -1
  198. package/dist/types/responses.d.mts +5 -1
  199. package/dist/types/vehicle.d.cts +7 -1
  200. package/dist/types/vehicle.d.mts +7 -1
  201. package/dist/types.d.cts +3 -3
  202. package/dist/types.d.mts +3 -3
  203. package/dist/util.cjs +41 -0
  204. package/dist/util.d.cts +18 -0
  205. package/dist/util.d.mts +18 -0
  206. package/dist/util.mjs +40 -0
  207. package/dist/vehicle.cjs +79 -36
  208. package/dist/vehicle.d.cts +42 -26
  209. package/dist/vehicle.d.mts +42 -26
  210. package/dist/vehicle.mjs +80 -37
  211. package/dist/vehiclesigned.cjs +20 -5
  212. package/dist/vehiclesigned.d.cts +14 -5
  213. package/dist/vehiclesigned.d.mts +14 -5
  214. package/dist/vehiclesigned.mjs +20 -5
  215. package/dist/vehiclespecific.cjs +126 -15
  216. package/dist/vehiclespecific.d.cts +82 -18
  217. package/dist/vehiclespecific.d.mts +82 -18
  218. package/dist/vehiclespecific.mjs +126 -15
  219. package/package.json +70 -3
@@ -0,0 +1,203 @@
1
+ const require_exceptions = require("../exceptions.cjs");
2
+ // src/router/base.ts
3
+ /**
4
+ * Failures in one backend's own path, before the device judged the command, so
5
+ * the next backend (a different path to the same device) may still succeed.
6
+ * Anything else, typically the device answering and refusing, propagates: the
7
+ * next backend reaches the same device and would get the same answer.
8
+ */
9
+ const FAILOVER_SAFE_ERRORS = [
10
+ require_exceptions.TeslaFleetConnectionError,
11
+ require_exceptions.BluetoothTransportError,
12
+ require_exceptions.BluetoothTimeout,
13
+ require_exceptions.BluetoothCommandFailed,
14
+ require_exceptions.BluetoothCloudUnavailableError,
15
+ require_exceptions.VehicleOffline,
16
+ require_exceptions.RateLimited,
17
+ require_exceptions.InternalServerError,
18
+ require_exceptions.BadGateway,
19
+ require_exceptions.ServiceUnavailable,
20
+ require_exceptions.GatewayTimeout,
21
+ ...require_exceptions.KEY_REJECTED_FAULTS,
22
+ require_exceptions.SessionInfoAuthenticationFault,
23
+ require_exceptions.SigningDisabled,
24
+ require_exceptions.SignedCommandRequired,
25
+ require_exceptions.TeslaFleetMessageFaultFieldExceedsMTU,
26
+ require_exceptions.TeslaFleetMessageFaultResponseSizeExceedsMTU,
27
+ require_exceptions.InvalidToken,
28
+ require_exceptions.MissingToken,
29
+ require_exceptions.OAuthExpired,
30
+ require_exceptions.OAuthInvalid,
31
+ require_exceptions.LoginRequired,
32
+ require_exceptions.PaymentRequired,
33
+ require_exceptions.SubscriptionRequired,
34
+ require_exceptions.VehicleSubscriptionRequired,
35
+ require_exceptions.InsufficientCredits,
36
+ require_exceptions.Forbidden,
37
+ require_exceptions.InvalidScope
38
+ ];
39
+ /**
40
+ * Whether a command that threw `error` may already have executed. The field
41
+ * check is the one that matters across the dual-package hazard: a process that
42
+ * loads this package through both `import` and `require` has two copies of the
43
+ * class, and `instanceof` fails across them.
44
+ */
45
+ function is_unconfirmed(error) {
46
+ return error instanceof require_exceptions.BluetoothUnconfirmedCommand || error?.unconfirmed === true;
47
+ }
48
+ /**
49
+ * Whether a call that threw `error` may be retried on the next backend. Accepts
50
+ * any thrown value. A boolean `failover_safe` on the value decides for itself,
51
+ * in both directions, which lets a backend the router cannot import mark its own
52
+ * errors. Otherwise it is safe only as an instance of `FAILOVER_SAFE_ERRORS`.
53
+ *
54
+ * An unconfirmed command is never safe, whatever it is marked. Python's
55
+ * `is_failover_safe` answers true for it (it subclasses `BluetoothTimeout`) and
56
+ * relies on the router checking first; here the predicate is safe to use alone.
57
+ */
58
+ function is_failover_safe(error) {
59
+ if (is_unconfirmed(error)) return false;
60
+ const marker = error?.failover_safe;
61
+ if (typeof marker === "boolean") return marker;
62
+ return FAILOVER_SAFE_ERRORS.some((cls) => error instanceof cls);
63
+ }
64
+ /**
65
+ * The names `backend` routes: plain function properties, own or inherited, up
66
+ * to `Object.prototype`. The most-derived property of a name decides, so a
67
+ * subclass that shadows a method with a getter or a value stops it routing.
68
+ * Getters are never invoked, and symbol keys (`Symbol.asyncDispose`) are not
69
+ * seen, so a context manager is not proxied.
70
+ */
71
+ function methodNames(backend) {
72
+ const names = /* @__PURE__ */ new Set();
73
+ const seen = /* @__PURE__ */ new Set();
74
+ for (let target = backend; target !== null && target !== Object.prototype; target = Object.getPrototypeOf(target)) for (const name of Object.getOwnPropertyNames(target)) {
75
+ if (seen.has(name)) continue;
76
+ seen.add(name);
77
+ if (name === "constructor" || name === "then" || name.startsWith("_")) continue;
78
+ if (typeof Object.getOwnPropertyDescriptor(target, name)?.value === "function") names.add(name);
79
+ }
80
+ return names;
81
+ }
82
+ /**
83
+ * A `TeslaFleetError`'s message is its class's, so it is safe to log. Anything
84
+ * else may hold a secret, so only its type is logged (see `src/log.ts`).
85
+ */
86
+ function errorText(error) {
87
+ if (error instanceof require_exceptions.TeslaFleetError) return `${error.name}: ${error.message}`;
88
+ return error instanceof Error ? error.name : typeof error;
89
+ }
90
+ /**
91
+ * Routes calls across an ordered list of backends, trying each in order.
92
+ *
93
+ * Use {@link VehicleRouter} or `EnergySiteRouter`, which type the routed
94
+ * methods. Methods are derived from the backends' prototypes at construction, so
95
+ * a new backend method is routed with no change here. A name on no backend does
96
+ * not exist. A backend without the method is skipped. A method added to a backend
97
+ * after construction is not routed.
98
+ *
99
+ * Each call is attempted on the backends that have the method. If one throws a
100
+ * value `is_failover_safe` accepts, the same call is retried on the next with
101
+ * the same arguments. Anything else propagates at once. When every backend fails
102
+ * over, the last thrown value is rethrown unchanged, even if it is not an `Error`.
103
+ *
104
+ * **Warning:** a failed call is replayed on the next backend, so a non-idempotent
105
+ * command (`honk_horn`, `actuate_trunk`, `door_unlock`, `charge_start`) that fails
106
+ * mid-flight, after a backend may have partly applied it, can be double-executed,
107
+ * or executed more than once across a longer chain. This is the accepted tradeoff
108
+ * of per-command failover. Callers that need exactly-once semantics gate dispatch
109
+ * with a health check or call a backend directly.
110
+ *
111
+ * `BluetoothUnconfirmedCommand` is the exception. The command may already have
112
+ * executed, so it propagates at once on any backend in the chain, is never
113
+ * replayed, and never reaches `on_result`.
114
+ *
115
+ * The health check gates the first backend only: when it is false the primary is
116
+ * skipped and the call routes to the rest. There is deliberately no per-backend
117
+ * health matrix.
118
+ *
119
+ * Plain attributes are not routed; read them from `primary` or `backends`. A
120
+ * backend's connection lifecycle is not proxied either.
121
+ *
122
+ * TS `private` methods exist at run time, so they are installed too: the types
123
+ * hide them. `#private` members are unreachable, which keeps a signing key off
124
+ * the router as it is off the backend.
125
+ */
126
+ var Router = class {
127
+ backends;
128
+ #health;
129
+ #on_result;
130
+ #logger;
131
+ constructor(backends, options = {}) {
132
+ if (backends.length < 2) throw new TypeError("A router needs a primary and a secondary backend.");
133
+ this.backends = Object.freeze([...backends]);
134
+ this.#health = options.health;
135
+ this.#on_result = options.on_result;
136
+ this.#logger = options.logger;
137
+ const targets = /* @__PURE__ */ new Map();
138
+ for (const backend of backends) for (const name of methodNames(backend)) targets.set(name, [...targets.get(name) ?? [], backend]);
139
+ for (const [name, having] of targets) {
140
+ if (name in this) continue;
141
+ Object.defineProperty(this, name, { value: (...args) => this.#dispatch(name, having, args) });
142
+ }
143
+ }
144
+ /** The first backend, tried first when healthy. */
145
+ get primary() {
146
+ return this.backends[0];
147
+ }
148
+ /** The second backend, tried when the primary is unhealthy or fails. */
149
+ get secondary() {
150
+ return this.backends[1];
151
+ }
152
+ /** Resolve the health check. With none configured this reports `true`. */
153
+ async is_healthy() {
154
+ const health = this.#health;
155
+ if (health == null) return true;
156
+ if (typeof health === "boolean") return health;
157
+ return Boolean(await health());
158
+ }
159
+ /** The `key=value` log field naming a backend. */
160
+ _backend_field(backend) {
161
+ return `backend=${backend.constructor?.name ?? "Object"}`;
162
+ }
163
+ /** `name` on the first backend that has it, for a plain attribute getter. */
164
+ _first_attribute(name) {
165
+ return this.backends.find((candidate) => name in candidate)?.[name];
166
+ }
167
+ async #dispatch(name, targets, args) {
168
+ let start = 0;
169
+ if (this.#health != null && targets.length > 1 && targets[0] === this.backends[0] && !await this.is_healthy()) start = 1;
170
+ let last;
171
+ for (const backend of targets.slice(start)) {
172
+ const field = this._backend_field(backend);
173
+ let result;
174
+ try {
175
+ result = await backend[name](...args);
176
+ } catch (error) {
177
+ if (is_unconfirmed(error)) {
178
+ this.#logger?.debug(`command=${name} ${field} result=unconfirmed error=${errorText(error)}`);
179
+ throw error;
180
+ }
181
+ last = error;
182
+ this.#logger?.debug(`command=${name} ${field} result=error error=${errorText(error)}`);
183
+ if (!(this.#on_result ? await this.#on_result(error, backend, name) : is_failover_safe(error))) throw error;
184
+ continue;
185
+ }
186
+ this.#logger?.debug(`command=${name} ${field} result=success`);
187
+ await this.#on_result?.(null, backend, name);
188
+ return result;
189
+ }
190
+ throw last;
191
+ }
192
+ };
193
+ var VehicleRouterBase = class extends Router {
194
+ /** The VIN of the first backend that has one. */
195
+ get vin() {
196
+ return this._first_attribute("vin");
197
+ }
198
+ };
199
+ const VehicleRouter = VehicleRouterBase;
200
+ exports.FAILOVER_SAFE_ERRORS = FAILOVER_SAFE_ERRORS;
201
+ exports.Router = Router;
202
+ exports.VehicleRouter = VehicleRouter;
203
+ exports.is_failover_safe = is_failover_safe;
@@ -0,0 +1,112 @@
1
+ import { Logger } from "../log.cjs";
2
+ type MethodKeys<B> = { [K in keyof B]-?: K extends `_${string}` | "then" ? never : B[K] extends ((...args: never[]) => unknown) ? K : never; }[keyof B];
3
+ type AllMethodKeys<B> = B extends unknown ? MethodKeys<B> : never;
4
+ export type MemberOf<B, K extends PropertyKey> = B extends { [P in K]: infer F; } ? F : never;
5
+ type Asynced<F> = F extends ((...args: infer A) => infer R) ? (...args: A) => Promise<Awaited<R>> : never;
6
+ /** Every method of the union `B`. A name on several members must satisfy all their signatures. */
7
+ export type Routed<B> = { [K in AllMethodKeys<B>]: Asynced<MemberOf<B, K>>; };
8
+ /** A primary, a secondary, and any number of further backends, tried in order. */
9
+ export type Backends = readonly [object, object, ...object[]];
10
+ /**
11
+ * Gates the primary. Omit it to attempt the primary directly and fail over on a
12
+ * failover-safe error, with no up-front probe.
13
+ */
14
+ export type HealthCheck = boolean | (() => boolean | Promise<boolean>);
15
+ /**
16
+ * Called after every dispatched call. On failure, `error` is the thrown value
17
+ * and the return decides whether failover continues, replacing
18
+ * `is_failover_safe` in both directions; return `is_failover_safe(error)` to keep
19
+ * the default policy. On success, `error` is `null` and the return is ignored.
20
+ * A handler that throws propagates. `BluetoothUnconfirmedCommand` never gets here.
21
+ */
22
+ export type ResultHandler = (error: unknown, backend: object, method: string) => boolean | Promise<boolean>;
23
+ export interface RouterOptions {
24
+ health?: HealthCheck | null;
25
+ on_result?: ResultHandler | null;
26
+ /** Receives one `command= backend= result=` line per attempted backend. Silent by default. */
27
+ logger?: Logger;
28
+ }
29
+ /**
30
+ * Failures in one backend's own path, before the device judged the command, so
31
+ * the next backend (a different path to the same device) may still succeed.
32
+ * Anything else, typically the device answering and refusing, propagates: the
33
+ * next backend reaches the same device and would get the same answer.
34
+ */
35
+ export declare const FAILOVER_SAFE_ERRORS: readonly (abstract new (...args: never[]) => unknown)[];
36
+ /**
37
+ * Whether a call that threw `error` may be retried on the next backend. Accepts
38
+ * any thrown value. A boolean `failover_safe` on the value decides for itself,
39
+ * in both directions, which lets a backend the router cannot import mark its own
40
+ * errors. Otherwise it is safe only as an instance of `FAILOVER_SAFE_ERRORS`.
41
+ *
42
+ * An unconfirmed command is never safe, whatever it is marked. Python's
43
+ * `is_failover_safe` answers true for it (it subclasses `BluetoothTimeout`) and
44
+ * relies on the router checking first; here the predicate is safe to use alone.
45
+ */
46
+ export declare function is_failover_safe(error: unknown): boolean;
47
+ /**
48
+ * Routes calls across an ordered list of backends, trying each in order.
49
+ *
50
+ * Use {@link VehicleRouter} or `EnergySiteRouter`, which type the routed
51
+ * methods. Methods are derived from the backends' prototypes at construction, so
52
+ * a new backend method is routed with no change here. A name on no backend does
53
+ * not exist. A backend without the method is skipped. A method added to a backend
54
+ * after construction is not routed.
55
+ *
56
+ * Each call is attempted on the backends that have the method. If one throws a
57
+ * value `is_failover_safe` accepts, the same call is retried on the next with
58
+ * the same arguments. Anything else propagates at once. When every backend fails
59
+ * over, the last thrown value is rethrown unchanged, even if it is not an `Error`.
60
+ *
61
+ * **Warning:** a failed call is replayed on the next backend, so a non-idempotent
62
+ * command (`honk_horn`, `actuate_trunk`, `door_unlock`, `charge_start`) that fails
63
+ * mid-flight, after a backend may have partly applied it, can be double-executed,
64
+ * or executed more than once across a longer chain. This is the accepted tradeoff
65
+ * of per-command failover. Callers that need exactly-once semantics gate dispatch
66
+ * with a health check or call a backend directly.
67
+ *
68
+ * `BluetoothUnconfirmedCommand` is the exception. The command may already have
69
+ * executed, so it propagates at once on any backend in the chain, is never
70
+ * replayed, and never reaches `on_result`.
71
+ *
72
+ * The health check gates the first backend only: when it is false the primary is
73
+ * skipped and the call routes to the rest. There is deliberately no per-backend
74
+ * health matrix.
75
+ *
76
+ * Plain attributes are not routed; read them from `primary` or `backends`. A
77
+ * backend's connection lifecycle is not proxied either.
78
+ *
79
+ * TS `private` methods exist at run time, so they are installed too: the types
80
+ * hide them. `#private` members are unreachable, which keeps a signing key off
81
+ * the router as it is off the backend.
82
+ */
83
+ export declare class Router<Bs extends Backends> {
84
+ #private;
85
+ readonly backends: Bs;
86
+ constructor(backends: Bs, options?: RouterOptions);
87
+ /** The first backend, tried first when healthy. */
88
+ get primary(): Bs[0];
89
+ /** The second backend, tried when the primary is unhealthy or fails. */
90
+ get secondary(): Bs[1];
91
+ /** Resolve the health check. With none configured this reports `true`. */
92
+ is_healthy(): Promise<boolean>;
93
+ /** The `key=value` log field naming a backend. */
94
+ protected _backend_field(backend: object): string;
95
+ /** `name` on the first backend that has it, for a plain attribute getter. */
96
+ protected _first_attribute<K extends string>(name: K): MemberOf<Bs[number], K>;
97
+ }
98
+ declare class VehicleRouterBase<Bs extends Backends> extends Router<Bs> {
99
+ /** The VIN of the first backend that has one. */
100
+ get vin(): MemberOf<Bs[number], "vin">;
101
+ }
102
+ /**
103
+ * A router over vehicles, typically a Bluetooth primary and a cloud fallback.
104
+ * See {@link Router} for dispatch, failover and the replay warning.
105
+ *
106
+ * A same-named method must accept the caller's arguments on every backend that
107
+ * has it, or the call does not compile: Python only fails at run time. Where two
108
+ * backends legitimately differ, call `router.primary.x()`.
109
+ */
110
+ export type VehicleRouter<Bs extends Backends> = VehicleRouterBase<Bs> & Routed<Bs[number]>;
111
+ export declare const VehicleRouter: new <const Bs extends Backends>(backends: Bs, options?: RouterOptions) => VehicleRouter<Bs>;
112
+ export {};
@@ -0,0 +1,112 @@
1
+ import { Logger } from "../log.mjs";
2
+ type MethodKeys<B> = { [K in keyof B]-?: K extends `_${string}` | "then" ? never : B[K] extends ((...args: never[]) => unknown) ? K : never; }[keyof B];
3
+ type AllMethodKeys<B> = B extends unknown ? MethodKeys<B> : never;
4
+ export type MemberOf<B, K extends PropertyKey> = B extends { [P in K]: infer F; } ? F : never;
5
+ type Asynced<F> = F extends ((...args: infer A) => infer R) ? (...args: A) => Promise<Awaited<R>> : never;
6
+ /** Every method of the union `B`. A name on several members must satisfy all their signatures. */
7
+ export type Routed<B> = { [K in AllMethodKeys<B>]: Asynced<MemberOf<B, K>>; };
8
+ /** A primary, a secondary, and any number of further backends, tried in order. */
9
+ export type Backends = readonly [object, object, ...object[]];
10
+ /**
11
+ * Gates the primary. Omit it to attempt the primary directly and fail over on a
12
+ * failover-safe error, with no up-front probe.
13
+ */
14
+ export type HealthCheck = boolean | (() => boolean | Promise<boolean>);
15
+ /**
16
+ * Called after every dispatched call. On failure, `error` is the thrown value
17
+ * and the return decides whether failover continues, replacing
18
+ * `is_failover_safe` in both directions; return `is_failover_safe(error)` to keep
19
+ * the default policy. On success, `error` is `null` and the return is ignored.
20
+ * A handler that throws propagates. `BluetoothUnconfirmedCommand` never gets here.
21
+ */
22
+ export type ResultHandler = (error: unknown, backend: object, method: string) => boolean | Promise<boolean>;
23
+ export interface RouterOptions {
24
+ health?: HealthCheck | null;
25
+ on_result?: ResultHandler | null;
26
+ /** Receives one `command= backend= result=` line per attempted backend. Silent by default. */
27
+ logger?: Logger;
28
+ }
29
+ /**
30
+ * Failures in one backend's own path, before the device judged the command, so
31
+ * the next backend (a different path to the same device) may still succeed.
32
+ * Anything else, typically the device answering and refusing, propagates: the
33
+ * next backend reaches the same device and would get the same answer.
34
+ */
35
+ export declare const FAILOVER_SAFE_ERRORS: readonly (abstract new (...args: never[]) => unknown)[];
36
+ /**
37
+ * Whether a call that threw `error` may be retried on the next backend. Accepts
38
+ * any thrown value. A boolean `failover_safe` on the value decides for itself,
39
+ * in both directions, which lets a backend the router cannot import mark its own
40
+ * errors. Otherwise it is safe only as an instance of `FAILOVER_SAFE_ERRORS`.
41
+ *
42
+ * An unconfirmed command is never safe, whatever it is marked. Python's
43
+ * `is_failover_safe` answers true for it (it subclasses `BluetoothTimeout`) and
44
+ * relies on the router checking first; here the predicate is safe to use alone.
45
+ */
46
+ export declare function is_failover_safe(error: unknown): boolean;
47
+ /**
48
+ * Routes calls across an ordered list of backends, trying each in order.
49
+ *
50
+ * Use {@link VehicleRouter} or `EnergySiteRouter`, which type the routed
51
+ * methods. Methods are derived from the backends' prototypes at construction, so
52
+ * a new backend method is routed with no change here. A name on no backend does
53
+ * not exist. A backend without the method is skipped. A method added to a backend
54
+ * after construction is not routed.
55
+ *
56
+ * Each call is attempted on the backends that have the method. If one throws a
57
+ * value `is_failover_safe` accepts, the same call is retried on the next with
58
+ * the same arguments. Anything else propagates at once. When every backend fails
59
+ * over, the last thrown value is rethrown unchanged, even if it is not an `Error`.
60
+ *
61
+ * **Warning:** a failed call is replayed on the next backend, so a non-idempotent
62
+ * command (`honk_horn`, `actuate_trunk`, `door_unlock`, `charge_start`) that fails
63
+ * mid-flight, after a backend may have partly applied it, can be double-executed,
64
+ * or executed more than once across a longer chain. This is the accepted tradeoff
65
+ * of per-command failover. Callers that need exactly-once semantics gate dispatch
66
+ * with a health check or call a backend directly.
67
+ *
68
+ * `BluetoothUnconfirmedCommand` is the exception. The command may already have
69
+ * executed, so it propagates at once on any backend in the chain, is never
70
+ * replayed, and never reaches `on_result`.
71
+ *
72
+ * The health check gates the first backend only: when it is false the primary is
73
+ * skipped and the call routes to the rest. There is deliberately no per-backend
74
+ * health matrix.
75
+ *
76
+ * Plain attributes are not routed; read them from `primary` or `backends`. A
77
+ * backend's connection lifecycle is not proxied either.
78
+ *
79
+ * TS `private` methods exist at run time, so they are installed too: the types
80
+ * hide them. `#private` members are unreachable, which keeps a signing key off
81
+ * the router as it is off the backend.
82
+ */
83
+ export declare class Router<Bs extends Backends> {
84
+ #private;
85
+ readonly backends: Bs;
86
+ constructor(backends: Bs, options?: RouterOptions);
87
+ /** The first backend, tried first when healthy. */
88
+ get primary(): Bs[0];
89
+ /** The second backend, tried when the primary is unhealthy or fails. */
90
+ get secondary(): Bs[1];
91
+ /** Resolve the health check. With none configured this reports `true`. */
92
+ is_healthy(): Promise<boolean>;
93
+ /** The `key=value` log field naming a backend. */
94
+ protected _backend_field(backend: object): string;
95
+ /** `name` on the first backend that has it, for a plain attribute getter. */
96
+ protected _first_attribute<K extends string>(name: K): MemberOf<Bs[number], K>;
97
+ }
98
+ declare class VehicleRouterBase<Bs extends Backends> extends Router<Bs> {
99
+ /** The VIN of the first backend that has one. */
100
+ get vin(): MemberOf<Bs[number], "vin">;
101
+ }
102
+ /**
103
+ * A router over vehicles, typically a Bluetooth primary and a cloud fallback.
104
+ * See {@link Router} for dispatch, failover and the replay warning.
105
+ *
106
+ * A same-named method must accept the caller's arguments on every backend that
107
+ * has it, or the call does not compile: Python only fails at run time. Where two
108
+ * backends legitimately differ, call `router.primary.x()`.
109
+ */
110
+ export type VehicleRouter<Bs extends Backends> = VehicleRouterBase<Bs> & Routed<Bs[number]>;
111
+ export declare const VehicleRouter: new <const Bs extends Backends>(backends: Bs, options?: RouterOptions) => VehicleRouter<Bs>;
112
+ export {};
@@ -0,0 +1,200 @@
1
+ import { BadGateway, BluetoothCloudUnavailableError, BluetoothCommandFailed, BluetoothTimeout, BluetoothTransportError, BluetoothUnconfirmedCommand, Forbidden, GatewayTimeout, InsufficientCredits, InternalServerError, InvalidScope, InvalidToken, KEY_REJECTED_FAULTS, LoginRequired, MissingToken, OAuthExpired, OAuthInvalid, PaymentRequired, RateLimited, ServiceUnavailable, SessionInfoAuthenticationFault, SignedCommandRequired, SigningDisabled, SubscriptionRequired, TeslaFleetConnectionError, TeslaFleetError, TeslaFleetMessageFaultFieldExceedsMTU, TeslaFleetMessageFaultResponseSizeExceedsMTU, VehicleOffline, VehicleSubscriptionRequired } from "../exceptions.mjs";
2
+ // src/router/base.ts
3
+ /**
4
+ * Failures in one backend's own path, before the device judged the command, so
5
+ * the next backend (a different path to the same device) may still succeed.
6
+ * Anything else, typically the device answering and refusing, propagates: the
7
+ * next backend reaches the same device and would get the same answer.
8
+ */
9
+ const FAILOVER_SAFE_ERRORS = [
10
+ TeslaFleetConnectionError,
11
+ BluetoothTransportError,
12
+ BluetoothTimeout,
13
+ BluetoothCommandFailed,
14
+ BluetoothCloudUnavailableError,
15
+ VehicleOffline,
16
+ RateLimited,
17
+ InternalServerError,
18
+ BadGateway,
19
+ ServiceUnavailable,
20
+ GatewayTimeout,
21
+ ...KEY_REJECTED_FAULTS,
22
+ SessionInfoAuthenticationFault,
23
+ SigningDisabled,
24
+ SignedCommandRequired,
25
+ TeslaFleetMessageFaultFieldExceedsMTU,
26
+ TeslaFleetMessageFaultResponseSizeExceedsMTU,
27
+ InvalidToken,
28
+ MissingToken,
29
+ OAuthExpired,
30
+ OAuthInvalid,
31
+ LoginRequired,
32
+ PaymentRequired,
33
+ SubscriptionRequired,
34
+ VehicleSubscriptionRequired,
35
+ InsufficientCredits,
36
+ Forbidden,
37
+ InvalidScope
38
+ ];
39
+ /**
40
+ * Whether a command that threw `error` may already have executed. The field
41
+ * check is the one that matters across the dual-package hazard: a process that
42
+ * loads this package through both `import` and `require` has two copies of the
43
+ * class, and `instanceof` fails across them.
44
+ */
45
+ function is_unconfirmed(error) {
46
+ return error instanceof BluetoothUnconfirmedCommand || error?.unconfirmed === true;
47
+ }
48
+ /**
49
+ * Whether a call that threw `error` may be retried on the next backend. Accepts
50
+ * any thrown value. A boolean `failover_safe` on the value decides for itself,
51
+ * in both directions, which lets a backend the router cannot import mark its own
52
+ * errors. Otherwise it is safe only as an instance of `FAILOVER_SAFE_ERRORS`.
53
+ *
54
+ * An unconfirmed command is never safe, whatever it is marked. Python's
55
+ * `is_failover_safe` answers true for it (it subclasses `BluetoothTimeout`) and
56
+ * relies on the router checking first; here the predicate is safe to use alone.
57
+ */
58
+ function is_failover_safe(error) {
59
+ if (is_unconfirmed(error)) return false;
60
+ const marker = error?.failover_safe;
61
+ if (typeof marker === "boolean") return marker;
62
+ return FAILOVER_SAFE_ERRORS.some((cls) => error instanceof cls);
63
+ }
64
+ /**
65
+ * The names `backend` routes: plain function properties, own or inherited, up
66
+ * to `Object.prototype`. The most-derived property of a name decides, so a
67
+ * subclass that shadows a method with a getter or a value stops it routing.
68
+ * Getters are never invoked, and symbol keys (`Symbol.asyncDispose`) are not
69
+ * seen, so a context manager is not proxied.
70
+ */
71
+ function methodNames(backend) {
72
+ const names = /* @__PURE__ */ new Set();
73
+ const seen = /* @__PURE__ */ new Set();
74
+ for (let target = backend; target !== null && target !== Object.prototype; target = Object.getPrototypeOf(target)) for (const name of Object.getOwnPropertyNames(target)) {
75
+ if (seen.has(name)) continue;
76
+ seen.add(name);
77
+ if (name === "constructor" || name === "then" || name.startsWith("_")) continue;
78
+ if (typeof Object.getOwnPropertyDescriptor(target, name)?.value === "function") names.add(name);
79
+ }
80
+ return names;
81
+ }
82
+ /**
83
+ * A `TeslaFleetError`'s message is its class's, so it is safe to log. Anything
84
+ * else may hold a secret, so only its type is logged (see `src/log.ts`).
85
+ */
86
+ function errorText(error) {
87
+ if (error instanceof TeslaFleetError) return `${error.name}: ${error.message}`;
88
+ return error instanceof Error ? error.name : typeof error;
89
+ }
90
+ /**
91
+ * Routes calls across an ordered list of backends, trying each in order.
92
+ *
93
+ * Use {@link VehicleRouter} or `EnergySiteRouter`, which type the routed
94
+ * methods. Methods are derived from the backends' prototypes at construction, so
95
+ * a new backend method is routed with no change here. A name on no backend does
96
+ * not exist. A backend without the method is skipped. A method added to a backend
97
+ * after construction is not routed.
98
+ *
99
+ * Each call is attempted on the backends that have the method. If one throws a
100
+ * value `is_failover_safe` accepts, the same call is retried on the next with
101
+ * the same arguments. Anything else propagates at once. When every backend fails
102
+ * over, the last thrown value is rethrown unchanged, even if it is not an `Error`.
103
+ *
104
+ * **Warning:** a failed call is replayed on the next backend, so a non-idempotent
105
+ * command (`honk_horn`, `actuate_trunk`, `door_unlock`, `charge_start`) that fails
106
+ * mid-flight, after a backend may have partly applied it, can be double-executed,
107
+ * or executed more than once across a longer chain. This is the accepted tradeoff
108
+ * of per-command failover. Callers that need exactly-once semantics gate dispatch
109
+ * with a health check or call a backend directly.
110
+ *
111
+ * `BluetoothUnconfirmedCommand` is the exception. The command may already have
112
+ * executed, so it propagates at once on any backend in the chain, is never
113
+ * replayed, and never reaches `on_result`.
114
+ *
115
+ * The health check gates the first backend only: when it is false the primary is
116
+ * skipped and the call routes to the rest. There is deliberately no per-backend
117
+ * health matrix.
118
+ *
119
+ * Plain attributes are not routed; read them from `primary` or `backends`. A
120
+ * backend's connection lifecycle is not proxied either.
121
+ *
122
+ * TS `private` methods exist at run time, so they are installed too: the types
123
+ * hide them. `#private` members are unreachable, which keeps a signing key off
124
+ * the router as it is off the backend.
125
+ */
126
+ var Router = class {
127
+ backends;
128
+ #health;
129
+ #on_result;
130
+ #logger;
131
+ constructor(backends, options = {}) {
132
+ if (backends.length < 2) throw new TypeError("A router needs a primary and a secondary backend.");
133
+ this.backends = Object.freeze([...backends]);
134
+ this.#health = options.health;
135
+ this.#on_result = options.on_result;
136
+ this.#logger = options.logger;
137
+ const targets = /* @__PURE__ */ new Map();
138
+ for (const backend of backends) for (const name of methodNames(backend)) targets.set(name, [...targets.get(name) ?? [], backend]);
139
+ for (const [name, having] of targets) {
140
+ if (name in this) continue;
141
+ Object.defineProperty(this, name, { value: (...args) => this.#dispatch(name, having, args) });
142
+ }
143
+ }
144
+ /** The first backend, tried first when healthy. */
145
+ get primary() {
146
+ return this.backends[0];
147
+ }
148
+ /** The second backend, tried when the primary is unhealthy or fails. */
149
+ get secondary() {
150
+ return this.backends[1];
151
+ }
152
+ /** Resolve the health check. With none configured this reports `true`. */
153
+ async is_healthy() {
154
+ const health = this.#health;
155
+ if (health == null) return true;
156
+ if (typeof health === "boolean") return health;
157
+ return Boolean(await health());
158
+ }
159
+ /** The `key=value` log field naming a backend. */
160
+ _backend_field(backend) {
161
+ return `backend=${backend.constructor?.name ?? "Object"}`;
162
+ }
163
+ /** `name` on the first backend that has it, for a plain attribute getter. */
164
+ _first_attribute(name) {
165
+ return this.backends.find((candidate) => name in candidate)?.[name];
166
+ }
167
+ async #dispatch(name, targets, args) {
168
+ let start = 0;
169
+ if (this.#health != null && targets.length > 1 && targets[0] === this.backends[0] && !await this.is_healthy()) start = 1;
170
+ let last;
171
+ for (const backend of targets.slice(start)) {
172
+ const field = this._backend_field(backend);
173
+ let result;
174
+ try {
175
+ result = await backend[name](...args);
176
+ } catch (error) {
177
+ if (is_unconfirmed(error)) {
178
+ this.#logger?.debug(`command=${name} ${field} result=unconfirmed error=${errorText(error)}`);
179
+ throw error;
180
+ }
181
+ last = error;
182
+ this.#logger?.debug(`command=${name} ${field} result=error error=${errorText(error)}`);
183
+ if (!(this.#on_result ? await this.#on_result(error, backend, name) : is_failover_safe(error))) throw error;
184
+ continue;
185
+ }
186
+ this.#logger?.debug(`command=${name} ${field} result=success`);
187
+ await this.#on_result?.(null, backend, name);
188
+ return result;
189
+ }
190
+ throw last;
191
+ }
192
+ };
193
+ var VehicleRouterBase = class extends Router {
194
+ /** The VIN of the first backend that has one. */
195
+ get vin() {
196
+ return this._first_attribute("vin");
197
+ }
198
+ };
199
+ const VehicleRouter = VehicleRouterBase;
200
+ export { FAILOVER_SAFE_ERRORS, Router, VehicleRouter, is_failover_safe };