tesla-fleet-api 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +284 -1
- package/dist/_virtual/_rolldown/runtime.cjs +10 -0
- package/dist/bluetooth/adapters/shared.cjs +38 -0
- package/dist/bluetooth/adapters/shared.mjs +36 -0
- package/dist/bluetooth/async.cjs +121 -0
- package/dist/bluetooth/async.d.cts +10 -0
- package/dist/bluetooth/async.d.mts +10 -0
- package/dist/bluetooth/async.mjs +116 -0
- package/dist/bluetooth/broadcast.cjs +153 -0
- package/dist/bluetooth/broadcast.d.cts +59 -0
- package/dist/bluetooth/broadcast.d.mts +59 -0
- package/dist/bluetooth/broadcast.mjs +151 -0
- package/dist/bluetooth/constants.cjs +99 -0
- package/dist/bluetooth/constants.d.cts +70 -0
- package/dist/bluetooth/constants.d.mts +70 -0
- package/dist/bluetooth/constants.mjs +72 -0
- package/dist/bluetooth/esphome.cjs +292 -0
- package/dist/bluetooth/esphome.d.cts +83 -0
- package/dist/bluetooth/esphome.d.mts +83 -0
- package/dist/bluetooth/esphome.mjs +291 -0
- package/dist/bluetooth/framing.cjs +125 -0
- package/dist/bluetooth/framing.d.cts +45 -0
- package/dist/bluetooth/framing.d.mts +45 -0
- package/dist/bluetooth/framing.mjs +122 -0
- package/dist/bluetooth/homey.cjs +155 -0
- package/dist/bluetooth/homey.d.cts +75 -0
- package/dist/bluetooth/homey.d.mts +75 -0
- package/dist/bluetooth/homey.mjs +150 -0
- package/dist/bluetooth/names.cjs +24 -0
- package/dist/bluetooth/names.d.cts +10 -0
- package/dist/bluetooth/names.d.mts +10 -0
- package/dist/bluetooth/names.mjs +20 -0
- package/dist/bluetooth/noble.cjs +178 -0
- package/dist/bluetooth/noble.d.cts +58 -0
- package/dist/bluetooth/noble.d.mts +58 -0
- package/dist/bluetooth/noble.mjs +177 -0
- package/dist/bluetooth/streamglue.cjs +157 -0
- package/dist/bluetooth/streamglue.d.cts +37 -0
- package/dist/bluetooth/streamglue.d.mts +37 -0
- package/dist/bluetooth/streamglue.mjs +154 -0
- package/dist/bluetooth/teslabluetooth.cjs +67 -0
- package/dist/bluetooth/teslabluetooth.d.cts +41 -0
- package/dist/bluetooth/teslabluetooth.d.mts +41 -0
- package/dist/bluetooth/teslabluetooth.mjs +66 -0
- package/dist/bluetooth/transport.cjs +10 -0
- package/dist/bluetooth/transport.d.cts +95 -0
- package/dist/bluetooth/transport.d.mts +95 -0
- package/dist/bluetooth/transport.mjs +10 -0
- package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
- package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
- package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
- package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
- package/dist/bluetooth/verify.cjs +93 -0
- package/dist/bluetooth/verify.d.cts +35 -0
- package/dist/bluetooth/verify.d.mts +35 -0
- package/dist/bluetooth/verify.mjs +91 -0
- package/dist/bluetooth.cjs +53 -0
- package/dist/bluetooth.d.cts +10 -0
- package/dist/bluetooth.d.mts +10 -0
- package/dist/bluetooth.mjs +9 -0
- package/dist/commands.cjs +1080 -85
- package/dist/commands.d.cts +585 -54
- package/dist/commands.d.mts +583 -51
- package/dist/commands.mjs +1084 -93
- package/dist/const.cjs +22 -0
- package/dist/const.d.cts +21 -0
- package/dist/const.d.mts +21 -0
- package/dist/const.mjs +20 -1
- package/dist/energy.cjs +24 -19
- package/dist/energy.d.cts +21 -15
- package/dist/energy.d.mts +21 -15
- package/dist/energy.mjs +24 -19
- package/dist/energyisland.cjs +38 -0
- package/dist/energyisland.mjs +37 -0
- package/dist/energyparsers.cjs +118 -0
- package/dist/energyparsers.d.cts +39 -0
- package/dist/energyparsers.d.mts +39 -0
- package/dist/energyparsers.mjs +116 -0
- package/dist/energyspecific.cjs +375 -17
- package/dist/energyspecific.d.cts +199 -19
- package/dist/energyspecific.d.mts +197 -16
- package/dist/energyspecific.mjs +370 -18
- package/dist/exceptions.cjs +3 -2
- package/dist/exceptions.d.cts +3 -2
- package/dist/exceptions.d.mts +3 -2
- package/dist/exceptions.mjs +3 -2
- package/dist/funnel.cjs +297 -0
- package/dist/funnel.d.cts +118 -0
- package/dist/funnel.d.mts +118 -0
- package/dist/funnel.mjs +292 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +22 -8
- package/dist/index.d.mts +22 -8
- package/dist/index.mjs +19 -6
- package/dist/log.cjs +75 -0
- package/dist/log.d.cts +4 -0
- package/dist/log.d.mts +4 -0
- package/dist/log.mjs +71 -0
- package/dist/oauth.cjs +143 -0
- package/dist/oauth.d.cts +69 -0
- package/dist/oauth.d.mts +69 -0
- package/dist/oauth.mjs +143 -0
- package/dist/powerwall/client.cjs +818 -0
- package/dist/powerwall/client.d.cts +297 -0
- package/dist/powerwall/client.d.mts +297 -0
- package/dist/powerwall/client.mjs +807 -0
- package/dist/powerwall/components.cjs +188 -0
- package/dist/powerwall/components.d.cts +45 -0
- package/dist/powerwall/components.d.mts +45 -0
- package/dist/powerwall/components.mjs +182 -0
- package/dist/powerwall/energysite.cjs +216 -0
- package/dist/powerwall/energysite.d.cts +104 -0
- package/dist/powerwall/energysite.d.mts +104 -0
- package/dist/powerwall/energysite.mjs +214 -0
- package/dist/powerwall/exceptions.cjs +59 -0
- package/dist/powerwall/exceptions.d.cts +38 -0
- package/dist/powerwall/exceptions.d.mts +38 -0
- package/dist/powerwall/exceptions.mjs +51 -0
- package/dist/powerwall/gateway.cjs +102 -0
- package/dist/powerwall/gateway.d.cts +57 -0
- package/dist/powerwall/gateway.d.mts +57 -0
- package/dist/powerwall/gateway.mjs +101 -0
- package/dist/powerwall/merge.cjs +163 -0
- package/dist/powerwall/merge.d.cts +63 -0
- package/dist/powerwall/merge.d.mts +63 -0
- package/dist/powerwall/merge.mjs +159 -0
- package/dist/powerwall/models.d.cts +95 -0
- package/dist/powerwall/models.d.mts +95 -0
- package/dist/powerwall/queries.cjs +17 -0
- package/dist/powerwall/queries.mjs +10 -0
- package/dist/powerwall/transport.cjs +232 -0
- package/dist/powerwall/transport.mjs +227 -0
- package/dist/powerwall.cjs +47 -0
- package/dist/powerwall.d.cts +9 -0
- package/dist/powerwall.d.mts +9 -0
- package/dist/powerwall.mjs +8 -0
- package/dist/router/base.cjs +203 -0
- package/dist/router/base.d.cts +112 -0
- package/dist/router/base.d.mts +112 -0
- package/dist/router/base.mjs +200 -0
- package/dist/router/energysite.cjs +65 -0
- package/dist/router/energysite.d.cts +47 -0
- package/dist/router/energysite.d.mts +47 -0
- package/dist/router/energysite.mjs +60 -0
- package/dist/router.cjs +13 -0
- package/dist/router.d.cts +3 -1
- package/dist/router.d.mts +3 -1
- package/dist/router.mjs +3 -1
- package/dist/signing/crypto.cjs +28 -0
- package/dist/signing/crypto.mjs +26 -1
- package/dist/signing/energy.cjs +123 -0
- package/dist/signing/energy.d.cts +45 -0
- package/dist/signing/energy.d.mts +45 -0
- package/dist/signing/energy.mjs +120 -0
- package/dist/signing/jws.cjs +229 -0
- package/dist/signing/jws.d.cts +45 -0
- package/dist/signing/jws.d.mts +45 -0
- package/dist/signing/jws.mjs +211 -0
- package/dist/signing/keys.cjs +191 -0
- package/dist/signing/keys.d.cts +21 -0
- package/dist/signing/keys.d.mts +21 -0
- package/dist/signing/keys.mjs +182 -0
- package/dist/signing/session.cjs +104 -30
- package/dist/signing/session.d.cts +60 -23
- package/dist/signing/session.d.mts +60 -23
- package/dist/signing/session.mjs +105 -31
- package/dist/tariff.cjs +27 -0
- package/dist/tariff.d.cts +11 -0
- package/dist/tariff.d.mts +11 -0
- package/dist/tariff.mjs +27 -1
- package/dist/teslafleetapi.cjs +256 -55
- package/dist/teslafleetapi.d.cts +110 -14
- package/dist/teslafleetapi.d.mts +110 -14
- package/dist/teslafleetapi.mjs +258 -57
- package/dist/teslemetry.cjs +152 -5
- package/dist/teslemetry.d.cts +95 -5
- package/dist/teslemetry.d.mts +93 -3
- package/dist/teslemetry.mjs +151 -6
- package/dist/teslemetryenergy.cjs +287 -0
- package/dist/teslemetryenergy.d.cts +154 -0
- package/dist/teslemetryenergy.d.mts +154 -0
- package/dist/teslemetryenergy.mjs +282 -0
- package/dist/teslemetryvehicle.cjs +407 -0
- package/dist/teslemetryvehicle.d.cts +238 -0
- package/dist/teslemetryvehicle.d.mts +238 -0
- package/dist/teslemetryvehicle.mjs +405 -0
- package/dist/tessie.cjs +42 -5
- package/dist/tessie.d.cts +51 -5
- package/dist/tessie.d.mts +49 -3
- package/dist/tessie.mjs +42 -5
- package/dist/tessievehicle.cjs +514 -0
- package/dist/tessievehicle.d.cts +333 -0
- package/dist/tessievehicle.d.mts +333 -0
- package/dist/tessievehicle.mjs +513 -0
- package/dist/types/live_status.d.cts +16 -16
- package/dist/types/live_status.d.mts +16 -16
- package/dist/types/responses.d.cts +5 -1
- package/dist/types/responses.d.mts +5 -1
- package/dist/types/vehicle.d.cts +7 -1
- package/dist/types/vehicle.d.mts +7 -1
- package/dist/types.d.cts +3 -3
- package/dist/types.d.mts +3 -3
- package/dist/util.cjs +41 -0
- package/dist/util.d.cts +18 -0
- package/dist/util.d.mts +18 -0
- package/dist/util.mjs +40 -0
- package/dist/vehicle.cjs +79 -36
- package/dist/vehicle.d.cts +42 -26
- package/dist/vehicle.d.mts +42 -26
- package/dist/vehicle.mjs +80 -37
- package/dist/vehiclesigned.cjs +20 -5
- package/dist/vehiclesigned.d.cts +14 -5
- package/dist/vehiclesigned.d.mts +14 -5
- package/dist/vehiclesigned.mjs +20 -5
- package/dist/vehiclespecific.cjs +126 -15
- package/dist/vehiclespecific.d.cts +82 -18
- package/dist/vehiclespecific.d.mts +82 -18
- package/dist/vehiclespecific.mjs +126 -15
- package/package.json +70 -3
package/dist/funnel.cjs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
const require_log = require("./log.cjs");
|
|
2
|
+
let _teslemetry_tesla_protocol_command_vcsec = require("@teslemetry/tesla-protocol/command/vcsec");
|
|
3
|
+
// src/funnel.ts
|
|
4
|
+
/**
|
|
5
|
+
* A canonical routable field, valued to match its public signal name. A compound
|
|
6
|
+
* signal is split into one path per facet, because a broadcast and a
|
|
7
|
+
* `vehicle_data` result each expose individual leaves.
|
|
8
|
+
*/
|
|
9
|
+
const FieldPath = Object.freeze({
|
|
10
|
+
LOCKED: "Locked",
|
|
11
|
+
CHARGE_PORT_DOOR_OPEN: "ChargePortDoorOpen",
|
|
12
|
+
DOOR_STATE_TRUNK_FRONT: "DoorState.TrunkFront"
|
|
13
|
+
});
|
|
14
|
+
function noop() {}
|
|
15
|
+
/**
|
|
16
|
+
* Funnels observations from every attached publisher into one listener set.
|
|
17
|
+
*
|
|
18
|
+
* Many to one: a listener registered for a field receives what any publisher
|
|
19
|
+
* reports for it, in arrival order. There is no source selection, ranking, or
|
|
20
|
+
* health model here, and detaching or silencing a publisher produces no value of
|
|
21
|
+
* its own. The only arbitration, dropping older observations and skipping
|
|
22
|
+
* unchanged values, is hard-coded.
|
|
23
|
+
*/
|
|
24
|
+
var ObservationFunnel = class {
|
|
25
|
+
#publishers = [];
|
|
26
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
27
|
+
#demandObservers = [];
|
|
28
|
+
#observed = /* @__PURE__ */ new Map();
|
|
29
|
+
#logger;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.#logger = options.logger;
|
|
32
|
+
}
|
|
33
|
+
/** Attach `publisher` and return its detach closure. */
|
|
34
|
+
attach(publisher) {
|
|
35
|
+
const attached = {
|
|
36
|
+
publisher,
|
|
37
|
+
detach: noop
|
|
38
|
+
};
|
|
39
|
+
this.#publishers.push(attached);
|
|
40
|
+
attached.detach = publisher.attach(this);
|
|
41
|
+
const active = this.#activePaths(publisher);
|
|
42
|
+
if (active.size > 0) publisher.request(active);
|
|
43
|
+
return () => {
|
|
44
|
+
const index = this.#publishers.indexOf(attached);
|
|
45
|
+
if (index === -1) return;
|
|
46
|
+
this.#publishers.splice(index, 1);
|
|
47
|
+
const stillActive = this.#activePaths(publisher);
|
|
48
|
+
if (stillActive.size > 0) publisher.release(stillActive);
|
|
49
|
+
attached.detach();
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
#activePaths(publisher) {
|
|
53
|
+
return new Set([...publisher.paths].filter((path) => this.#listeners.get(path)?.length));
|
|
54
|
+
}
|
|
55
|
+
/** Accept one observation from any publisher and fan it out. */
|
|
56
|
+
publish(observation) {
|
|
57
|
+
const { path } = observation;
|
|
58
|
+
const previous = this.#observed.get(path);
|
|
59
|
+
if (previous !== void 0 && observation.observed_at < previous.observed_at) return;
|
|
60
|
+
this.#observed.set(path, observation);
|
|
61
|
+
if (previous !== void 0 && previous.value === observation.value) return;
|
|
62
|
+
for (const callback of [...this.#listeners.get(path) ?? []]) {
|
|
63
|
+
if (this.#observed.get(path) !== observation) return;
|
|
64
|
+
this.#dispatch(callback, observation.value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The last value observed for `path`. Null means there is no reading: either
|
|
69
|
+
* nothing has ever been observed, or a source reported the field as unavailable.
|
|
70
|
+
*/
|
|
71
|
+
value(path) {
|
|
72
|
+
return this.#observed.get(path)?.value ?? null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Register a listener for `path`, called on each changed value. The first
|
|
76
|
+
* listener for a path activates it on every capable publisher; the last one
|
|
77
|
+
* to leave releases it. Registration dispatches nothing: the value standing at
|
|
78
|
+
* that moment is `value()`.
|
|
79
|
+
*/
|
|
80
|
+
listen(path, callback) {
|
|
81
|
+
let listeners = this.#listeners.get(path);
|
|
82
|
+
if (listeners === void 0) {
|
|
83
|
+
listeners = [];
|
|
84
|
+
this.#listeners.set(path, listeners);
|
|
85
|
+
}
|
|
86
|
+
const registered = listeners;
|
|
87
|
+
registered.push(callback);
|
|
88
|
+
if (registered.length === 1) this.#forCapable(path, (publisher, paths) => publisher.request(paths));
|
|
89
|
+
this.#notifyDemand();
|
|
90
|
+
let released = false;
|
|
91
|
+
return () => {
|
|
92
|
+
if (released) return;
|
|
93
|
+
released = true;
|
|
94
|
+
registered.splice(registered.indexOf(callback), 1);
|
|
95
|
+
if (registered.length === 0) this.#forCapable(path, (publisher, paths) => publisher.release(paths));
|
|
96
|
+
this.#notifyDemand();
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
#forCapable(path, action) {
|
|
100
|
+
const paths = /* @__PURE__ */ new Set([path]);
|
|
101
|
+
for (const { publisher } of [...this.#publishers]) if (publisher.paths.has(path)) action(publisher, paths);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Observe whether any of `paths` has at least one live listener. Derived from
|
|
105
|
+
* the activation counts the funnel already keeps. It reports; it never starts
|
|
106
|
+
* work of its own.
|
|
107
|
+
*/
|
|
108
|
+
listen_demand(paths, callback) {
|
|
109
|
+
const observer = {
|
|
110
|
+
paths,
|
|
111
|
+
callback,
|
|
112
|
+
active: this.#demand(paths)
|
|
113
|
+
};
|
|
114
|
+
this.#demandObservers.push(observer);
|
|
115
|
+
this.#dispatch(callback, observer.active);
|
|
116
|
+
let released = false;
|
|
117
|
+
return () => {
|
|
118
|
+
if (released) return;
|
|
119
|
+
released = true;
|
|
120
|
+
const index = this.#demandObservers.indexOf(observer);
|
|
121
|
+
if (index !== -1) this.#demandObservers.splice(index, 1);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
#demand(paths) {
|
|
125
|
+
return [...paths].some((path) => this.#listeners.get(path)?.length);
|
|
126
|
+
}
|
|
127
|
+
#notifyDemand() {
|
|
128
|
+
for (const observer of [...this.#demandObservers]) {
|
|
129
|
+
const active = this.#demand(observer.paths);
|
|
130
|
+
if (active !== observer.active) {
|
|
131
|
+
observer.active = active;
|
|
132
|
+
this.#dispatch(observer.callback, active);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** A listener's error stays with that listener: the rest still hear the value. */
|
|
137
|
+
#dispatch(callback, value) {
|
|
138
|
+
try {
|
|
139
|
+
callback(value);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.#logger?.debug(`observation funnel listener callback failed error=${require_log.errorText(error)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const LOCK_STATES = /* @__PURE__ */ new Map([
|
|
146
|
+
[_teslemetry_tesla_protocol_command_vcsec.VehicleLockStateE.VEHICLELOCKSTATE_LOCKED, true],
|
|
147
|
+
[_teslemetry_tesla_protocol_command_vcsec.VehicleLockStateE.VEHICLELOCKSTATE_UNLOCKED, false],
|
|
148
|
+
[_teslemetry_tesla_protocol_command_vcsec.VehicleLockStateE.VEHICLELOCKSTATE_INTERNAL_LOCKED, true],
|
|
149
|
+
[_teslemetry_tesla_protocol_command_vcsec.VehicleLockStateE.VEHICLELOCKSTATE_SELECTIVE_UNLOCKED, false]
|
|
150
|
+
]);
|
|
151
|
+
const CLOSURE_STATES = /* @__PURE__ */ new Map([
|
|
152
|
+
[_teslemetry_tesla_protocol_command_vcsec.ClosureStateE.CLOSURESTATE_CLOSED, false],
|
|
153
|
+
[_teslemetry_tesla_protocol_command_vcsec.ClosureStateE.CLOSURESTATE_OPEN, true],
|
|
154
|
+
[_teslemetry_tesla_protocol_command_vcsec.ClosureStateE.CLOSURESTATE_AJAR, true],
|
|
155
|
+
[_teslemetry_tesla_protocol_command_vcsec.ClosureStateE.CLOSURESTATE_OPENING, true],
|
|
156
|
+
[_teslemetry_tesla_protocol_command_vcsec.ClosureStateE.CLOSURESTATE_CLOSING, true]
|
|
157
|
+
]);
|
|
158
|
+
/** `paths` of a passive publisher: every field. */
|
|
159
|
+
const ALL_PATHS = new Set(Object.values(FieldPath));
|
|
160
|
+
const monotonicSeconds = () => performance.now() / 1e3;
|
|
161
|
+
const ABSENT = Symbol("absent");
|
|
162
|
+
function isMapping(value) {
|
|
163
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
164
|
+
}
|
|
165
|
+
/** A present leaf, or `ABSENT`: a missing key is not a null reading. */
|
|
166
|
+
function leaf(section, key) {
|
|
167
|
+
return isMapping(section) && Object.hasOwn(section, key) ? section[key] : ABSENT;
|
|
168
|
+
}
|
|
169
|
+
/** A value usable as a reading: a boolean, or null for an explicit "unavailable". */
|
|
170
|
+
function isReading(value) {
|
|
171
|
+
return value === null || typeof value === "boolean";
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A source whose readings are handed to it as arguments. It holds no client,
|
|
175
|
+
* session, endpoint, or callable able to obtain one, so nothing here can originate
|
|
176
|
+
* a request, and activation subscribes to nothing.
|
|
177
|
+
*/
|
|
178
|
+
var SuppliedPublisher = class {
|
|
179
|
+
#clock;
|
|
180
|
+
#sink = null;
|
|
181
|
+
constructor(options = {}) {
|
|
182
|
+
this.#clock = options.clock ?? monotonicSeconds;
|
|
183
|
+
}
|
|
184
|
+
get paths() {
|
|
185
|
+
return ALL_PATHS;
|
|
186
|
+
}
|
|
187
|
+
attach(sink) {
|
|
188
|
+
this.#sink = sink;
|
|
189
|
+
return () => {
|
|
190
|
+
this.#sink = null;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
request(_paths) {}
|
|
194
|
+
release(_paths) {}
|
|
195
|
+
_publish(input, observed_at) {
|
|
196
|
+
const observations = this._translate(input, observed_at ?? this.#clock());
|
|
197
|
+
const sink = this.#sink;
|
|
198
|
+
if (sink !== null) for (const observation of observations) sink.publish(observation);
|
|
199
|
+
return observations;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
/** Translates a caller-supplied `vehicle_data` result into observations. */
|
|
203
|
+
var VehicleDataResultPublisher = class extends SuppliedPublisher {
|
|
204
|
+
/** Translate a supplied result and feed the audited leaves to the sink. */
|
|
205
|
+
publish_result(result, options = {}) {
|
|
206
|
+
return this._publish(result, options.observed_at);
|
|
207
|
+
}
|
|
208
|
+
_translate(result, observed_at) {
|
|
209
|
+
const response = leaf(result, "response");
|
|
210
|
+
const payload = isMapping(response) ? response : result;
|
|
211
|
+
const vehicle_state = leaf(payload, "vehicle_state");
|
|
212
|
+
const charge_state = leaf(payload, "charge_state");
|
|
213
|
+
const observations = [];
|
|
214
|
+
const locked = leaf(vehicle_state, "locked");
|
|
215
|
+
if (isReading(locked)) observations.push({
|
|
216
|
+
path: FieldPath.LOCKED,
|
|
217
|
+
value: locked,
|
|
218
|
+
observed_at
|
|
219
|
+
});
|
|
220
|
+
const charge_port = leaf(charge_state, "charge_port_door_open");
|
|
221
|
+
if (isReading(charge_port)) observations.push({
|
|
222
|
+
path: FieldPath.CHARGE_PORT_DOOR_OPEN,
|
|
223
|
+
value: charge_port,
|
|
224
|
+
observed_at
|
|
225
|
+
});
|
|
226
|
+
const front_trunk = leaf(vehicle_state, "ft");
|
|
227
|
+
if (front_trunk === null) observations.push({
|
|
228
|
+
path: FieldPath.DOOR_STATE_TRUNK_FRONT,
|
|
229
|
+
value: null,
|
|
230
|
+
observed_at
|
|
231
|
+
});
|
|
232
|
+
else if (front_trunk === 0 || front_trunk === 1) observations.push({
|
|
233
|
+
path: FieldPath.DOOR_STATE_TRUNK_FRONT,
|
|
234
|
+
value: front_trunk === 1,
|
|
235
|
+
observed_at
|
|
236
|
+
});
|
|
237
|
+
return observations;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* `"true"` and `"false"` become real booleans; anything else passes through. Some
|
|
242
|
+
* vehicles stream a boolean signal as that literal string even when the stream
|
|
243
|
+
* prefers typed values.
|
|
244
|
+
*/
|
|
245
|
+
function coerceStreamBool(value) {
|
|
246
|
+
if (value === "true") return true;
|
|
247
|
+
if (value === "false") return false;
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Translates a caller-supplied Teslemetry stream signal update into observations.
|
|
252
|
+
* The update is keyed by signal name the way a stream push is (`Locked`,
|
|
253
|
+
* `ChargePortDoorOpen`, `DoorState` with a nested `TrunkFront`). `@teslemetry/api`
|
|
254
|
+
* owns the stream client; the two meet here.
|
|
255
|
+
*/
|
|
256
|
+
var TeslemetryStreamPublisher = class extends SuppliedPublisher {
|
|
257
|
+
/** Translate a supplied stream signal update and feed it to the sink. */
|
|
258
|
+
publish_update(data, options = {}) {
|
|
259
|
+
return this._publish(data, options.observed_at);
|
|
260
|
+
}
|
|
261
|
+
_translate(data, observed_at) {
|
|
262
|
+
const observations = [];
|
|
263
|
+
const locked = coerceStreamBool(leaf(data, "Locked"));
|
|
264
|
+
if (isReading(locked)) observations.push({
|
|
265
|
+
path: FieldPath.LOCKED,
|
|
266
|
+
value: locked,
|
|
267
|
+
observed_at
|
|
268
|
+
});
|
|
269
|
+
const charge_port = coerceStreamBool(leaf(data, "ChargePortDoorOpen"));
|
|
270
|
+
if (isReading(charge_port)) observations.push({
|
|
271
|
+
path: FieldPath.CHARGE_PORT_DOOR_OPEN,
|
|
272
|
+
value: charge_port,
|
|
273
|
+
observed_at
|
|
274
|
+
});
|
|
275
|
+
const door_state = leaf(data, "DoorState");
|
|
276
|
+
if (door_state === null) observations.push({
|
|
277
|
+
path: FieldPath.DOOR_STATE_TRUNK_FRONT,
|
|
278
|
+
value: null,
|
|
279
|
+
observed_at
|
|
280
|
+
});
|
|
281
|
+
else if (isMapping(door_state)) {
|
|
282
|
+
const trunk_front = coerceStreamBool(leaf(door_state, "TrunkFront"));
|
|
283
|
+
if (isReading(trunk_front)) observations.push({
|
|
284
|
+
path: FieldPath.DOOR_STATE_TRUNK_FRONT,
|
|
285
|
+
value: trunk_front,
|
|
286
|
+
observed_at
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return observations;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
exports.CLOSURE_STATES = CLOSURE_STATES;
|
|
293
|
+
exports.FieldPath = FieldPath;
|
|
294
|
+
exports.LOCK_STATES = LOCK_STATES;
|
|
295
|
+
exports.ObservationFunnel = ObservationFunnel;
|
|
296
|
+
exports.TeslemetryStreamPublisher = TeslemetryStreamPublisher;
|
|
297
|
+
exports.VehicleDataResultPublisher = VehicleDataResultPublisher;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Logger } from "./log.cjs";
|
|
2
|
+
export type Unsubscribe = () => void;
|
|
3
|
+
/** Seconds off a monotonic clock. */
|
|
4
|
+
export type Clock = () => number;
|
|
5
|
+
/** A reading, or null for a source reporting the field itself as unavailable. */
|
|
6
|
+
export type Value = boolean | null;
|
|
7
|
+
/**
|
|
8
|
+
* A canonical routable field, valued to match its public signal name. A compound
|
|
9
|
+
* signal is split into one path per facet, because a broadcast and a
|
|
10
|
+
* `vehicle_data` result each expose individual leaves.
|
|
11
|
+
*/
|
|
12
|
+
export declare const FieldPath: Readonly<{
|
|
13
|
+
readonly LOCKED: "Locked";
|
|
14
|
+
readonly CHARGE_PORT_DOOR_OPEN: "ChargePortDoorOpen";
|
|
15
|
+
readonly DOOR_STATE_TRUNK_FRONT: "DoorState.TrunkFront";
|
|
16
|
+
}>;
|
|
17
|
+
export type FieldPath = (typeof FieldPath)[keyof typeof FieldPath];
|
|
18
|
+
/** One timestamped value for one field. */
|
|
19
|
+
export interface Observation {
|
|
20
|
+
readonly path: FieldPath;
|
|
21
|
+
readonly value: Value;
|
|
22
|
+
/**
|
|
23
|
+
* Seconds off one monotonic clock shared by every publisher on a funnel;
|
|
24
|
+
* observations from different sources are ordered against each other.
|
|
25
|
+
*/
|
|
26
|
+
readonly observed_at: number;
|
|
27
|
+
}
|
|
28
|
+
/** The handle a publisher is given to feed the funnel. */
|
|
29
|
+
export interface ObservationSink {
|
|
30
|
+
publish(observation: Observation): void;
|
|
31
|
+
}
|
|
32
|
+
/** A source of observations for one or more canonical fields. */
|
|
33
|
+
export interface Publisher {
|
|
34
|
+
readonly paths: ReadonlySet<FieldPath>;
|
|
35
|
+
attach(sink: ObservationSink): Unsubscribe;
|
|
36
|
+
/** Begin supplying `paths`: subscription accounting, not a data request. */
|
|
37
|
+
request(paths: ReadonlySet<FieldPath>): void;
|
|
38
|
+
/** Stop supplying `paths`. */
|
|
39
|
+
release(paths: ReadonlySet<FieldPath>): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Funnels observations from every attached publisher into one listener set.
|
|
43
|
+
*
|
|
44
|
+
* Many to one: a listener registered for a field receives what any publisher
|
|
45
|
+
* reports for it, in arrival order. There is no source selection, ranking, or
|
|
46
|
+
* health model here, and detaching or silencing a publisher produces no value of
|
|
47
|
+
* its own. The only arbitration, dropping older observations and skipping
|
|
48
|
+
* unchanged values, is hard-coded.
|
|
49
|
+
*/
|
|
50
|
+
export declare class ObservationFunnel implements ObservationSink {
|
|
51
|
+
#private;
|
|
52
|
+
constructor(options?: {
|
|
53
|
+
logger?: Logger;
|
|
54
|
+
});
|
|
55
|
+
/** Attach `publisher` and return its detach closure. */
|
|
56
|
+
attach(publisher: Publisher): Unsubscribe;
|
|
57
|
+
/** Accept one observation from any publisher and fan it out. */
|
|
58
|
+
publish(observation: Observation): void;
|
|
59
|
+
/**
|
|
60
|
+
* The last value observed for `path`. Null means there is no reading: either
|
|
61
|
+
* nothing has ever been observed, or a source reported the field as unavailable.
|
|
62
|
+
*/
|
|
63
|
+
value(path: FieldPath): Value;
|
|
64
|
+
/**
|
|
65
|
+
* Register a listener for `path`, called on each changed value. The first
|
|
66
|
+
* listener for a path activates it on every capable publisher; the last one
|
|
67
|
+
* to leave releases it. Registration dispatches nothing: the value standing at
|
|
68
|
+
* that moment is `value()`.
|
|
69
|
+
*/
|
|
70
|
+
listen(path: FieldPath, callback: (value: Value) => void): Unsubscribe;
|
|
71
|
+
/**
|
|
72
|
+
* Observe whether any of `paths` has at least one live listener. Derived from
|
|
73
|
+
* the activation counts the funnel already keeps. It reports; it never starts
|
|
74
|
+
* work of its own.
|
|
75
|
+
*/
|
|
76
|
+
listen_demand(paths: ReadonlySet<FieldPath>, callback: (active: boolean) => void): Unsubscribe;
|
|
77
|
+
}
|
|
78
|
+
export declare const LOCK_STATES: ReadonlyMap<number, boolean>;
|
|
79
|
+
export declare const CLOSURE_STATES: ReadonlyMap<number, boolean>;
|
|
80
|
+
/**
|
|
81
|
+
* A source whose readings are handed to it as arguments. It holds no client,
|
|
82
|
+
* session, endpoint, or callable able to obtain one, so nothing here can originate
|
|
83
|
+
* a request, and activation subscribes to nothing.
|
|
84
|
+
*/
|
|
85
|
+
declare abstract class SuppliedPublisher<Input> implements Publisher {
|
|
86
|
+
#private;
|
|
87
|
+
constructor(options?: {
|
|
88
|
+
clock?: Clock;
|
|
89
|
+
});
|
|
90
|
+
get paths(): ReadonlySet<FieldPath>;
|
|
91
|
+
attach(sink: ObservationSink): Unsubscribe;
|
|
92
|
+
request(_paths: ReadonlySet<FieldPath>): void;
|
|
93
|
+
release(_paths: ReadonlySet<FieldPath>): void;
|
|
94
|
+
protected _publish(input: Input, observed_at: number | undefined): readonly Observation[];
|
|
95
|
+
protected abstract _translate(input: Input, observed_at: number): Observation[];
|
|
96
|
+
}
|
|
97
|
+
/** Translates a caller-supplied `vehicle_data` result into observations. */
|
|
98
|
+
export declare class VehicleDataResultPublisher extends SuppliedPublisher<unknown> {
|
|
99
|
+
/** Translate a supplied result and feed the audited leaves to the sink. */
|
|
100
|
+
publish_result(result: unknown, options?: {
|
|
101
|
+
observed_at?: number;
|
|
102
|
+
}): readonly Observation[];
|
|
103
|
+
protected _translate(result: unknown, observed_at: number): Observation[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Translates a caller-supplied Teslemetry stream signal update into observations.
|
|
107
|
+
* The update is keyed by signal name the way a stream push is (`Locked`,
|
|
108
|
+
* `ChargePortDoorOpen`, `DoorState` with a nested `TrunkFront`). `@teslemetry/api`
|
|
109
|
+
* owns the stream client; the two meet here.
|
|
110
|
+
*/
|
|
111
|
+
export declare class TeslemetryStreamPublisher extends SuppliedPublisher<unknown> {
|
|
112
|
+
/** Translate a supplied stream signal update and feed it to the sink. */
|
|
113
|
+
publish_update(data: unknown, options?: {
|
|
114
|
+
observed_at?: number;
|
|
115
|
+
}): readonly Observation[];
|
|
116
|
+
protected _translate(data: unknown, observed_at: number): Observation[];
|
|
117
|
+
}
|
|
118
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Logger } from "./log.mjs";
|
|
2
|
+
export type Unsubscribe = () => void;
|
|
3
|
+
/** Seconds off a monotonic clock. */
|
|
4
|
+
export type Clock = () => number;
|
|
5
|
+
/** A reading, or null for a source reporting the field itself as unavailable. */
|
|
6
|
+
export type Value = boolean | null;
|
|
7
|
+
/**
|
|
8
|
+
* A canonical routable field, valued to match its public signal name. A compound
|
|
9
|
+
* signal is split into one path per facet, because a broadcast and a
|
|
10
|
+
* `vehicle_data` result each expose individual leaves.
|
|
11
|
+
*/
|
|
12
|
+
export declare const FieldPath: Readonly<{
|
|
13
|
+
readonly LOCKED: "Locked";
|
|
14
|
+
readonly CHARGE_PORT_DOOR_OPEN: "ChargePortDoorOpen";
|
|
15
|
+
readonly DOOR_STATE_TRUNK_FRONT: "DoorState.TrunkFront";
|
|
16
|
+
}>;
|
|
17
|
+
export type FieldPath = (typeof FieldPath)[keyof typeof FieldPath];
|
|
18
|
+
/** One timestamped value for one field. */
|
|
19
|
+
export interface Observation {
|
|
20
|
+
readonly path: FieldPath;
|
|
21
|
+
readonly value: Value;
|
|
22
|
+
/**
|
|
23
|
+
* Seconds off one monotonic clock shared by every publisher on a funnel;
|
|
24
|
+
* observations from different sources are ordered against each other.
|
|
25
|
+
*/
|
|
26
|
+
readonly observed_at: number;
|
|
27
|
+
}
|
|
28
|
+
/** The handle a publisher is given to feed the funnel. */
|
|
29
|
+
export interface ObservationSink {
|
|
30
|
+
publish(observation: Observation): void;
|
|
31
|
+
}
|
|
32
|
+
/** A source of observations for one or more canonical fields. */
|
|
33
|
+
export interface Publisher {
|
|
34
|
+
readonly paths: ReadonlySet<FieldPath>;
|
|
35
|
+
attach(sink: ObservationSink): Unsubscribe;
|
|
36
|
+
/** Begin supplying `paths`: subscription accounting, not a data request. */
|
|
37
|
+
request(paths: ReadonlySet<FieldPath>): void;
|
|
38
|
+
/** Stop supplying `paths`. */
|
|
39
|
+
release(paths: ReadonlySet<FieldPath>): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Funnels observations from every attached publisher into one listener set.
|
|
43
|
+
*
|
|
44
|
+
* Many to one: a listener registered for a field receives what any publisher
|
|
45
|
+
* reports for it, in arrival order. There is no source selection, ranking, or
|
|
46
|
+
* health model here, and detaching or silencing a publisher produces no value of
|
|
47
|
+
* its own. The only arbitration, dropping older observations and skipping
|
|
48
|
+
* unchanged values, is hard-coded.
|
|
49
|
+
*/
|
|
50
|
+
export declare class ObservationFunnel implements ObservationSink {
|
|
51
|
+
#private;
|
|
52
|
+
constructor(options?: {
|
|
53
|
+
logger?: Logger;
|
|
54
|
+
});
|
|
55
|
+
/** Attach `publisher` and return its detach closure. */
|
|
56
|
+
attach(publisher: Publisher): Unsubscribe;
|
|
57
|
+
/** Accept one observation from any publisher and fan it out. */
|
|
58
|
+
publish(observation: Observation): void;
|
|
59
|
+
/**
|
|
60
|
+
* The last value observed for `path`. Null means there is no reading: either
|
|
61
|
+
* nothing has ever been observed, or a source reported the field as unavailable.
|
|
62
|
+
*/
|
|
63
|
+
value(path: FieldPath): Value;
|
|
64
|
+
/**
|
|
65
|
+
* Register a listener for `path`, called on each changed value. The first
|
|
66
|
+
* listener for a path activates it on every capable publisher; the last one
|
|
67
|
+
* to leave releases it. Registration dispatches nothing: the value standing at
|
|
68
|
+
* that moment is `value()`.
|
|
69
|
+
*/
|
|
70
|
+
listen(path: FieldPath, callback: (value: Value) => void): Unsubscribe;
|
|
71
|
+
/**
|
|
72
|
+
* Observe whether any of `paths` has at least one live listener. Derived from
|
|
73
|
+
* the activation counts the funnel already keeps. It reports; it never starts
|
|
74
|
+
* work of its own.
|
|
75
|
+
*/
|
|
76
|
+
listen_demand(paths: ReadonlySet<FieldPath>, callback: (active: boolean) => void): Unsubscribe;
|
|
77
|
+
}
|
|
78
|
+
export declare const LOCK_STATES: ReadonlyMap<number, boolean>;
|
|
79
|
+
export declare const CLOSURE_STATES: ReadonlyMap<number, boolean>;
|
|
80
|
+
/**
|
|
81
|
+
* A source whose readings are handed to it as arguments. It holds no client,
|
|
82
|
+
* session, endpoint, or callable able to obtain one, so nothing here can originate
|
|
83
|
+
* a request, and activation subscribes to nothing.
|
|
84
|
+
*/
|
|
85
|
+
declare abstract class SuppliedPublisher<Input> implements Publisher {
|
|
86
|
+
#private;
|
|
87
|
+
constructor(options?: {
|
|
88
|
+
clock?: Clock;
|
|
89
|
+
});
|
|
90
|
+
get paths(): ReadonlySet<FieldPath>;
|
|
91
|
+
attach(sink: ObservationSink): Unsubscribe;
|
|
92
|
+
request(_paths: ReadonlySet<FieldPath>): void;
|
|
93
|
+
release(_paths: ReadonlySet<FieldPath>): void;
|
|
94
|
+
protected _publish(input: Input, observed_at: number | undefined): readonly Observation[];
|
|
95
|
+
protected abstract _translate(input: Input, observed_at: number): Observation[];
|
|
96
|
+
}
|
|
97
|
+
/** Translates a caller-supplied `vehicle_data` result into observations. */
|
|
98
|
+
export declare class VehicleDataResultPublisher extends SuppliedPublisher<unknown> {
|
|
99
|
+
/** Translate a supplied result and feed the audited leaves to the sink. */
|
|
100
|
+
publish_result(result: unknown, options?: {
|
|
101
|
+
observed_at?: number;
|
|
102
|
+
}): readonly Observation[];
|
|
103
|
+
protected _translate(result: unknown, observed_at: number): Observation[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Translates a caller-supplied Teslemetry stream signal update into observations.
|
|
107
|
+
* The update is keyed by signal name the way a stream push is (`Locked`,
|
|
108
|
+
* `ChargePortDoorOpen`, `DoorState` with a nested `TrunkFront`). `@teslemetry/api`
|
|
109
|
+
* owns the stream client; the two meet here.
|
|
110
|
+
*/
|
|
111
|
+
export declare class TeslemetryStreamPublisher extends SuppliedPublisher<unknown> {
|
|
112
|
+
/** Translate a supplied stream signal update and feed it to the sink. */
|
|
113
|
+
publish_update(data: unknown, options?: {
|
|
114
|
+
observed_at?: number;
|
|
115
|
+
}): readonly Observation[];
|
|
116
|
+
protected _translate(data: unknown, observed_at: number): Observation[];
|
|
117
|
+
}
|
|
118
|
+
export {};
|