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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_async = require("./async.cjs");
|
|
3
|
+
const require_constants = require("./constants.cjs");
|
|
4
|
+
const require_transport = require("./transport.cjs");
|
|
5
|
+
const require_shared = require("./adapters/shared.cjs");
|
|
6
|
+
// src/bluetooth/adapters/noble.ts
|
|
7
|
+
function find(characteristics, uuid) {
|
|
8
|
+
return characteristics.find((characteristic) => require_shared.compactUuid(characteristic.uuid) === require_shared.compactUuid(uuid));
|
|
9
|
+
}
|
|
10
|
+
async function loadNoble(bindings) {
|
|
11
|
+
const specifier = "@stoprocent/noble/lib/resolve-bindings.js";
|
|
12
|
+
let module;
|
|
13
|
+
try {
|
|
14
|
+
module = await import(specifier);
|
|
15
|
+
} catch (cause) {
|
|
16
|
+
throw new Error("NobleBleTransport needs @stoprocent/noble, or a `noble` option.", { cause });
|
|
17
|
+
}
|
|
18
|
+
return module.default(bindings);
|
|
19
|
+
}
|
|
20
|
+
var NobleConnection = class {
|
|
21
|
+
peripheral;
|
|
22
|
+
handlers;
|
|
23
|
+
claims;
|
|
24
|
+
lost = false;
|
|
25
|
+
closed = false;
|
|
26
|
+
closing;
|
|
27
|
+
genericAccess;
|
|
28
|
+
characteristics = [];
|
|
29
|
+
onData = (data, isNotification) => {
|
|
30
|
+
if (isNotification !== false) this.handlers.onNotify(data);
|
|
31
|
+
};
|
|
32
|
+
onDisconnect = () => {
|
|
33
|
+
this.lost = true;
|
|
34
|
+
if (!this.closed) this.handlers.onDisconnect();
|
|
35
|
+
};
|
|
36
|
+
constructor(peripheral, handlers, claims) {
|
|
37
|
+
this.peripheral = peripheral;
|
|
38
|
+
this.handlers = handlers;
|
|
39
|
+
this.claims = claims;
|
|
40
|
+
}
|
|
41
|
+
get isConnected() {
|
|
42
|
+
return !this.lost && !this.closed && this.peripheral.state === "connected";
|
|
43
|
+
}
|
|
44
|
+
/** Read each time: noble can report a larger MTU after the link is up. */
|
|
45
|
+
get maxWriteSize() {
|
|
46
|
+
const { mtu } = this.peripheral;
|
|
47
|
+
return mtu && mtu > 3 ? mtu - 3 : 20;
|
|
48
|
+
}
|
|
49
|
+
/** Resolve the characteristics and turn notifications on. */
|
|
50
|
+
async subscribe() {
|
|
51
|
+
const { peripheral } = this;
|
|
52
|
+
const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync([require_shared.compactUuid(require_constants.SERVICE_UUID)], [
|
|
53
|
+
require_constants.VEHICLE_WRITE_UUID,
|
|
54
|
+
require_constants.VEHICLE_NOTIFY_UUID,
|
|
55
|
+
require_constants.VERSION_UUID
|
|
56
|
+
].map(require_shared.compactUuid));
|
|
57
|
+
const notify = find(characteristics, require_constants.VEHICLE_NOTIFY_UUID);
|
|
58
|
+
if (!notify || !find(characteristics, "00000212-b2d1-43f0-9b88-960cebf8b91e")) throw new Error("The vehicle's write or notify characteristic was not found.");
|
|
59
|
+
this.assertCurrent();
|
|
60
|
+
this.characteristics = characteristics;
|
|
61
|
+
notify.on("data", this.onData);
|
|
62
|
+
await notify.subscribeAsync();
|
|
63
|
+
}
|
|
64
|
+
/** A superseded attempt stops here, before it changes a link another attempt owns. */
|
|
65
|
+
assertCurrent() {
|
|
66
|
+
if (this.claims.superseded(this.peripheral.id, this)) throw new Error("A newer connect attempt took over this link.");
|
|
67
|
+
}
|
|
68
|
+
async write(data) {
|
|
69
|
+
const characteristic = find(this.characteristics, require_constants.VEHICLE_WRITE_UUID);
|
|
70
|
+
if (!characteristic) throw new require_transport.BleNotSubmittedError("The vehicle write characteristic was not found.");
|
|
71
|
+
if (!this.isConnected) throw new require_transport.BleNotSubmittedError("The link is down.");
|
|
72
|
+
await characteristic.writeAsync(Buffer.from(data), false);
|
|
73
|
+
}
|
|
74
|
+
async read(characteristic) {
|
|
75
|
+
const target = characteristic === "version" ? find(this.characteristics, require_constants.VERSION_UUID) : find(await this.discoverGenericAccess(), characteristic === "deviceName" ? require_constants.NAME_UUID : require_constants.APPEARANCE_UUID);
|
|
76
|
+
if (!target) throw new require_transport.BleNotSubmittedError(`The ${characteristic} characteristic was not found.`);
|
|
77
|
+
return target.readAsync();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* BlueZ and CoreBluetooth can hide this service. A failed lookup leaves a
|
|
81
|
+
* characteristic unresolved on a healthy link, so it must not read as a lost one.
|
|
82
|
+
*/
|
|
83
|
+
discoverGenericAccess() {
|
|
84
|
+
this.genericAccess ??= this.peripheral.discoverSomeServicesAndCharacteristicsAsync([require_shared.compactUuid(require_constants.GENERIC_ACCESS_UUID)], [require_shared.compactUuid(require_constants.NAME_UUID), require_shared.compactUuid(require_constants.APPEARANCE_UUID)]).then(({ characteristics }) => characteristics).catch((cause) => {
|
|
85
|
+
this.genericAccess = void 0;
|
|
86
|
+
throw new require_transport.BleNotSubmittedError("Generic Access could not be resolved.", { cause });
|
|
87
|
+
});
|
|
88
|
+
return this.genericAccess;
|
|
89
|
+
}
|
|
90
|
+
disconnect() {
|
|
91
|
+
this.closing ??= (async () => {
|
|
92
|
+
this.closed = true;
|
|
93
|
+
const { peripheral, claims } = this;
|
|
94
|
+
peripheral.removeListener("disconnect", this.onDisconnect);
|
|
95
|
+
find(this.characteristics, require_constants.VEHICLE_NOTIFY_UUID)?.removeListener("data", this.onData);
|
|
96
|
+
const superseded = claims.superseded(peripheral.id, this);
|
|
97
|
+
claims.release(peripheral.id, this);
|
|
98
|
+
if (!superseded && peripheral.state !== "disconnected") await require_shared.quietly(() => peripheral.disconnectAsync());
|
|
99
|
+
})();
|
|
100
|
+
return this.closing;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var NobleBleTransport = class {
|
|
104
|
+
options;
|
|
105
|
+
backend;
|
|
106
|
+
scans = new require_async.AsyncLock();
|
|
107
|
+
peripherals = /* @__PURE__ */ new Map();
|
|
108
|
+
claims = new require_shared.LinkClaims();
|
|
109
|
+
constructor(options = {}) {
|
|
110
|
+
this.options = options;
|
|
111
|
+
}
|
|
112
|
+
noble() {
|
|
113
|
+
if (this.options.noble) return Promise.resolve(this.options.noble);
|
|
114
|
+
const fallback = process.platform === "linux" ? "dbus" : "default";
|
|
115
|
+
const loading = this.backend ??= loadNoble(this.options.bindings ?? fallback);
|
|
116
|
+
loading.catch(() => {
|
|
117
|
+
this.backend = void 0;
|
|
118
|
+
});
|
|
119
|
+
return loading;
|
|
120
|
+
}
|
|
121
|
+
async scan({ timeoutMs, matches }) {
|
|
122
|
+
const noble = await this.noble();
|
|
123
|
+
return this.listen(noble, timeoutMs, (device) => device.name !== void 0 && matches?.(device.name) === true);
|
|
124
|
+
}
|
|
125
|
+
/** Scan with no service filter until `done` accepts a device or the window ends. */
|
|
126
|
+
listen(noble, timeoutMs, done) {
|
|
127
|
+
return this.scans.run(async () => {
|
|
128
|
+
await noble.waitForPoweredOnAsync(timeoutMs);
|
|
129
|
+
const seen = /* @__PURE__ */ new Map();
|
|
130
|
+
let finish;
|
|
131
|
+
const finished = new Promise((resolve) => {
|
|
132
|
+
finish = resolve;
|
|
133
|
+
});
|
|
134
|
+
const onDiscover = (peripheral) => {
|
|
135
|
+
this.peripherals.set(peripheral.id, peripheral);
|
|
136
|
+
const device = {
|
|
137
|
+
id: peripheral.id,
|
|
138
|
+
name: peripheral.advertisement.localName ?? seen.get(peripheral.id)?.name,
|
|
139
|
+
rssi: peripheral.rssi,
|
|
140
|
+
connectable: peripheral.connectable
|
|
141
|
+
};
|
|
142
|
+
seen.set(peripheral.id, device);
|
|
143
|
+
if (done(device)) finish();
|
|
144
|
+
};
|
|
145
|
+
noble.on("discover", onDiscover);
|
|
146
|
+
const timer = setTimeout(finish, Math.max(0, timeoutMs - 250));
|
|
147
|
+
try {
|
|
148
|
+
await noble.startScanningAsync([], true);
|
|
149
|
+
await finished;
|
|
150
|
+
} finally {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
noble.removeListener("discover", onDiscover);
|
|
153
|
+
await require_shared.quietly(() => noble.stopScanningAsync());
|
|
154
|
+
}
|
|
155
|
+
return [...seen.values()];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async connect(device, handlers) {
|
|
159
|
+
const noble = await this.noble();
|
|
160
|
+
if (!this.peripherals.has(device.id)) await this.listen(noble, require_constants.DEFAULT_SCAN_TIMEOUT_MS, ({ id }) => id === device.id);
|
|
161
|
+
const peripheral = this.peripherals.get(device.id);
|
|
162
|
+
if (!peripheral) throw new Error("The device was not seen in a scan.");
|
|
163
|
+
await require_shared.quietly(() => noble.stopScanningAsync());
|
|
164
|
+
const connection = new NobleConnection(peripheral, handlers, this.claims);
|
|
165
|
+
this.claims.claim(device.id, connection);
|
|
166
|
+
peripheral.once("disconnect", connection.onDisconnect);
|
|
167
|
+
try {
|
|
168
|
+
await peripheral.connectAsync();
|
|
169
|
+
connection.assertCurrent();
|
|
170
|
+
await require_async.withTimeout(() => connection.subscribe(), require_constants.DEFAULT_CONNECT_TIMEOUT_MS);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
await connection.disconnect();
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
return connection;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
exports.NobleBleTransport = NobleBleTransport;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BleConnection, BleDeviceRef, BleLinkHandlers, BleScanOptions, BleTransport } from "./transport.cjs";
|
|
2
|
+
export interface NobleCharacteristicLike {
|
|
3
|
+
uuid: string;
|
|
4
|
+
on(event: "data", listener: (data: Buffer, isNotification?: boolean) => void): unknown;
|
|
5
|
+
removeListener(event: "data", listener: (data: Buffer, isNotification?: boolean) => void): unknown;
|
|
6
|
+
subscribeAsync(): Promise<void>;
|
|
7
|
+
/** `withoutResponse: false` is a write with response. */
|
|
8
|
+
writeAsync(data: Buffer, withoutResponse: boolean): Promise<void>;
|
|
9
|
+
readAsync(): Promise<Buffer>;
|
|
10
|
+
}
|
|
11
|
+
export interface NoblePeripheralLike {
|
|
12
|
+
id: string;
|
|
13
|
+
state: string;
|
|
14
|
+
mtu: number | null;
|
|
15
|
+
rssi?: number;
|
|
16
|
+
connectable?: boolean;
|
|
17
|
+
advertisement: {
|
|
18
|
+
localName?: string;
|
|
19
|
+
};
|
|
20
|
+
connectAsync(): Promise<void>;
|
|
21
|
+
disconnectAsync(): Promise<void>;
|
|
22
|
+
discoverSomeServicesAndCharacteristicsAsync(serviceUUIDs: string[], characteristicUUIDs: string[]): Promise<{
|
|
23
|
+
characteristics: NobleCharacteristicLike[];
|
|
24
|
+
}>;
|
|
25
|
+
once(event: "disconnect", listener: () => void): unknown;
|
|
26
|
+
removeListener(event: "disconnect", listener: () => void): unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface NobleLike {
|
|
29
|
+
waitForPoweredOnAsync(timeout?: number): Promise<void>;
|
|
30
|
+
startScanningAsync(serviceUUIDs?: string[], allowDuplicates?: boolean): Promise<void>;
|
|
31
|
+
stopScanningAsync(): Promise<void>;
|
|
32
|
+
on(event: "discover", listener: (peripheral: NoblePeripheralLike) => void): unknown;
|
|
33
|
+
removeListener(event: "discover", listener: (peripheral: NoblePeripheralLike) => void): unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface NobleBleTransportOptions {
|
|
36
|
+
/** A noble you built, such as `withBindings("dbus")`. Without it, `@stoprocent/noble` loads on first use. */
|
|
37
|
+
noble?: NobleLike;
|
|
38
|
+
/**
|
|
39
|
+
* Which bindings to load. Defaults to `"dbus"` on Linux, which shares the
|
|
40
|
+
* adapter with BlueZ and needs no root but does need `dbus-next`, and to
|
|
41
|
+
* `"default"` elsewhere. `"hci"` needs `sudo setcap cap_net_raw+eip` on the
|
|
42
|
+
* Node binary.
|
|
43
|
+
*/
|
|
44
|
+
bindings?: "default" | "dbus" | "hci" | "mac" | "win";
|
|
45
|
+
}
|
|
46
|
+
export declare class NobleBleTransport implements BleTransport {
|
|
47
|
+
private readonly options;
|
|
48
|
+
private backend;
|
|
49
|
+
private readonly scans;
|
|
50
|
+
private readonly peripherals;
|
|
51
|
+
private readonly claims;
|
|
52
|
+
constructor(options?: NobleBleTransportOptions);
|
|
53
|
+
private noble;
|
|
54
|
+
scan({ timeoutMs, matches }: BleScanOptions): Promise<BleDeviceRef[]>;
|
|
55
|
+
/** Scan with no service filter until `done` accepts a device or the window ends. */
|
|
56
|
+
private listen;
|
|
57
|
+
connect(device: BleDeviceRef, handlers: BleLinkHandlers): Promise<BleConnection>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BleConnection, BleDeviceRef, BleLinkHandlers, BleScanOptions, BleTransport } from "./transport.mjs";
|
|
2
|
+
export interface NobleCharacteristicLike {
|
|
3
|
+
uuid: string;
|
|
4
|
+
on(event: "data", listener: (data: Buffer, isNotification?: boolean) => void): unknown;
|
|
5
|
+
removeListener(event: "data", listener: (data: Buffer, isNotification?: boolean) => void): unknown;
|
|
6
|
+
subscribeAsync(): Promise<void>;
|
|
7
|
+
/** `withoutResponse: false` is a write with response. */
|
|
8
|
+
writeAsync(data: Buffer, withoutResponse: boolean): Promise<void>;
|
|
9
|
+
readAsync(): Promise<Buffer>;
|
|
10
|
+
}
|
|
11
|
+
export interface NoblePeripheralLike {
|
|
12
|
+
id: string;
|
|
13
|
+
state: string;
|
|
14
|
+
mtu: number | null;
|
|
15
|
+
rssi?: number;
|
|
16
|
+
connectable?: boolean;
|
|
17
|
+
advertisement: {
|
|
18
|
+
localName?: string;
|
|
19
|
+
};
|
|
20
|
+
connectAsync(): Promise<void>;
|
|
21
|
+
disconnectAsync(): Promise<void>;
|
|
22
|
+
discoverSomeServicesAndCharacteristicsAsync(serviceUUIDs: string[], characteristicUUIDs: string[]): Promise<{
|
|
23
|
+
characteristics: NobleCharacteristicLike[];
|
|
24
|
+
}>;
|
|
25
|
+
once(event: "disconnect", listener: () => void): unknown;
|
|
26
|
+
removeListener(event: "disconnect", listener: () => void): unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface NobleLike {
|
|
29
|
+
waitForPoweredOnAsync(timeout?: number): Promise<void>;
|
|
30
|
+
startScanningAsync(serviceUUIDs?: string[], allowDuplicates?: boolean): Promise<void>;
|
|
31
|
+
stopScanningAsync(): Promise<void>;
|
|
32
|
+
on(event: "discover", listener: (peripheral: NoblePeripheralLike) => void): unknown;
|
|
33
|
+
removeListener(event: "discover", listener: (peripheral: NoblePeripheralLike) => void): unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface NobleBleTransportOptions {
|
|
36
|
+
/** A noble you built, such as `withBindings("dbus")`. Without it, `@stoprocent/noble` loads on first use. */
|
|
37
|
+
noble?: NobleLike;
|
|
38
|
+
/**
|
|
39
|
+
* Which bindings to load. Defaults to `"dbus"` on Linux, which shares the
|
|
40
|
+
* adapter with BlueZ and needs no root but does need `dbus-next`, and to
|
|
41
|
+
* `"default"` elsewhere. `"hci"` needs `sudo setcap cap_net_raw+eip` on the
|
|
42
|
+
* Node binary.
|
|
43
|
+
*/
|
|
44
|
+
bindings?: "default" | "dbus" | "hci" | "mac" | "win";
|
|
45
|
+
}
|
|
46
|
+
export declare class NobleBleTransport implements BleTransport {
|
|
47
|
+
private readonly options;
|
|
48
|
+
private backend;
|
|
49
|
+
private readonly scans;
|
|
50
|
+
private readonly peripherals;
|
|
51
|
+
private readonly claims;
|
|
52
|
+
constructor(options?: NobleBleTransportOptions);
|
|
53
|
+
private noble;
|
|
54
|
+
scan({ timeoutMs, matches }: BleScanOptions): Promise<BleDeviceRef[]>;
|
|
55
|
+
/** Scan with no service filter until `done` accepts a device or the window ends. */
|
|
56
|
+
private listen;
|
|
57
|
+
connect(device: BleDeviceRef, handlers: BleLinkHandlers): Promise<BleConnection>;
|
|
58
|
+
}
|
|
@@ -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
|
+
}
|