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,188 @@
|
|
|
1
|
+
// src/powerwall/components.ts
|
|
2
|
+
/** Idle PCH string currents read as float noise around 1e-16 A. */
|
|
3
|
+
const PV_CURRENT_NOISE_FLOOR_A = .001;
|
|
4
|
+
const SERIAL_KINDS = [
|
|
5
|
+
"hvp",
|
|
6
|
+
"bms",
|
|
7
|
+
"pch",
|
|
8
|
+
"baggr"
|
|
9
|
+
];
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
/** Walk nested objects and lists; `undefined` if a step is missing. */
|
|
14
|
+
function path(data, ...keys) {
|
|
15
|
+
let value = data;
|
|
16
|
+
for (const key of keys) if (typeof key === "number") value = Array.isArray(value) ? value[key] : void 0;
|
|
17
|
+
else value = isRecord(value) ? value[key] : void 0;
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
/** The `kind` component at `slot`, or `null`. */
|
|
21
|
+
function component(components, kind, slot = 0) {
|
|
22
|
+
const item = path(components, "components", kind, slot);
|
|
23
|
+
return isRecord(item) ? item : null;
|
|
24
|
+
}
|
|
25
|
+
/** One signal of a component: its `value`, else `textValue`, else `boolValue`, or `null` when none is set. */
|
|
26
|
+
function component_signal(components, kind, name, slot = 0) {
|
|
27
|
+
const signals = component(components, kind, slot)?.signals;
|
|
28
|
+
const signal = (Array.isArray(signals) ? signals : []).find((item) => isRecord(item) && item.name === name);
|
|
29
|
+
return signal?.value ?? signal?.textValue ?? signal?.boolValue ?? null;
|
|
30
|
+
}
|
|
31
|
+
/** A numeric signal, or `null` when it is missing or not a number. */
|
|
32
|
+
function component_float(components, kind, name, slot = 0) {
|
|
33
|
+
const value = component_signal(components, kind, name, slot);
|
|
34
|
+
return typeof value === "number" ? value : null;
|
|
35
|
+
}
|
|
36
|
+
/** PCH PV string current in amps, for a `string` from `A` to `F`. An idle string's noise reads as 0. */
|
|
37
|
+
function pv_string_current(components, string, slot = 0) {
|
|
38
|
+
const value = component_float(components, "pch", `PCH_PvCurrent${string}`, slot);
|
|
39
|
+
return value !== null && Math.abs(value) < PV_CURRENT_NOISE_FLOOR_A ? 0 : value;
|
|
40
|
+
}
|
|
41
|
+
function firstAcrossKinds(components, slot, field) {
|
|
42
|
+
for (const kind of SERIAL_KINDS) {
|
|
43
|
+
const value = path(components, "components", kind, slot, field);
|
|
44
|
+
if (typeof value === "string" && value) return value;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** The first non-empty serial number reported for `slot`. */
|
|
49
|
+
function component_serial(components, slot) {
|
|
50
|
+
return firstAcrossKinds(components, slot, "serialNumber");
|
|
51
|
+
}
|
|
52
|
+
/** The first non-empty part number reported for `slot`. */
|
|
53
|
+
function component_part_number(components, slot) {
|
|
54
|
+
return firstAcrossKinds(components, slot, "partNumber");
|
|
55
|
+
}
|
|
56
|
+
function unitDin(item) {
|
|
57
|
+
for (const key of ["din", "vin"]) {
|
|
58
|
+
const value = item[key];
|
|
59
|
+
if (typeof value === "string" && value) return value;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function serialFromDin(din) {
|
|
64
|
+
return din.split("--").at(-1);
|
|
65
|
+
}
|
|
66
|
+
function bmsFull(components, slot) {
|
|
67
|
+
return component_float(components, "bms", "BMS_nominalFullPackEnergy", slot);
|
|
68
|
+
}
|
|
69
|
+
/** Component slots that look like real BMS modules. */
|
|
70
|
+
function bmsSlots(components) {
|
|
71
|
+
const items = path(components, "components", "bms");
|
|
72
|
+
if (!Array.isArray(items)) return [];
|
|
73
|
+
return items.flatMap((_, slot) => (bmsFull(components, slot) ?? 0) > 0 ? [slot] : []);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* BMS slots after dropping phantom expansion rows. A gateway can keep a
|
|
77
|
+
* registered but uninstalled expansion: a plausible capacity, no serial and
|
|
78
|
+
* near-zero energy. Such rows are dropped only when the system aggregate already
|
|
79
|
+
* matches the kept rows, so a real pack with a stale reading stays.
|
|
80
|
+
*/
|
|
81
|
+
function ghostFilteredBmsSlots(components, status) {
|
|
82
|
+
const slots = bmsSlots(components);
|
|
83
|
+
const aggregateWh = path(status, "control", "systemStatus", "nominalFullPackEnergyWh");
|
|
84
|
+
if (slots.length === 0 || typeof aggregateWh !== "number" || aggregateWh <= 0) return slots;
|
|
85
|
+
const aggregateKwh = aggregateWh / 1e3;
|
|
86
|
+
const ghosts = slots.filter((slot) => {
|
|
87
|
+
if (component_serial(components, slot)) return false;
|
|
88
|
+
const full = bmsFull(components, slot);
|
|
89
|
+
const remaining = component_float(components, "bms", "BMS_nominalEnergyRemaining", slot);
|
|
90
|
+
if (full === null || full <= 0 || remaining === null) return false;
|
|
91
|
+
return remaining < .5 || remaining / full < .05;
|
|
92
|
+
});
|
|
93
|
+
if (ghosts.length === 0) return slots;
|
|
94
|
+
const kept = slots.filter((slot) => !ghosts.includes(slot));
|
|
95
|
+
const keptKwh = kept.reduce((sum, slot) => sum + (bmsFull(components, slot) ?? 0), 0);
|
|
96
|
+
return keptKwh > 0 && Math.abs(aggregateKwh - keptKwh) / aggregateKwh < .1 ? kept : slots;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Choose the component slots of follower Powerwalls. A slot matched to a
|
|
100
|
+
* configured expansion is never chosen. Candidates with no serial win when
|
|
101
|
+
* there are enough of them; otherwise the last candidates are taken.
|
|
102
|
+
*/
|
|
103
|
+
function followerSlots(components, powerwallCount, expansionSlots, slots) {
|
|
104
|
+
const followerCount = Math.max(0, powerwallCount - 1);
|
|
105
|
+
if (followerCount === 0) return [];
|
|
106
|
+
if (slots.length === 0) return Array.from({ length: followerCount }, (_, index) => index + 1);
|
|
107
|
+
const candidates = slots.filter((slot) => slot !== 0 && !expansionSlots.has(slot));
|
|
108
|
+
const noSerial = candidates.filter((slot) => !component_serial(components, slot));
|
|
109
|
+
const chosen = (noSerial.length >= followerCount ? noSerial : candidates).slice(-followerCount);
|
|
110
|
+
for (let fallback = 1; chosen.length < followerCount; fallback++) if (!chosen.includes(fallback) && !expansionSlots.has(fallback)) chosen.push(fallback);
|
|
111
|
+
return chosen.sort((a, b) => a - b);
|
|
112
|
+
}
|
|
113
|
+
function statusBatteryDins(status) {
|
|
114
|
+
const blocks = path(status, "control", "batteryBlocks");
|
|
115
|
+
if (!Array.isArray(blocks)) return [];
|
|
116
|
+
return blocks.filter(isRecord).flatMap((block) => unitDin(block) ?? []);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Every battery unit of the site with its component slot, by block: each
|
|
120
|
+
* Powerwall, then its expansions.
|
|
121
|
+
*
|
|
122
|
+
* Powerwalls come from `battery_blocks` plus any extra DIN in the status; the
|
|
123
|
+
* first is the leader at slot 0. Expansions come from each block's
|
|
124
|
+
* `battery_expansions` and match a slot by serial number. A configured expansion
|
|
125
|
+
* with no real BMS row is dropped, and a real row no configured unit claims
|
|
126
|
+
* becomes an expansion of the leader with `din: null`.
|
|
127
|
+
*/
|
|
128
|
+
function battery_units(config, status, components) {
|
|
129
|
+
const configured = config.battery_blocks;
|
|
130
|
+
const blocks = Array.isArray(configured) ? configured.filter(isRecord) : [];
|
|
131
|
+
const known = new Set(blocks.map(unitDin));
|
|
132
|
+
for (const din of statusBatteryDins(status)) if (!known.has(din)) {
|
|
133
|
+
blocks.push({ din });
|
|
134
|
+
known.add(din);
|
|
135
|
+
}
|
|
136
|
+
if (blocks.length === 0) blocks.push({});
|
|
137
|
+
const powerwallCount = blocks.length;
|
|
138
|
+
const expansionDinsByBlock = blocks.map((block) => {
|
|
139
|
+
const expansions = block.battery_expansions;
|
|
140
|
+
return Array.isArray(expansions) ? expansions.filter(isRecord).flatMap((item) => unitDin(item) ?? []) : [];
|
|
141
|
+
});
|
|
142
|
+
const slots = ghostFilteredBmsSlots(components, status);
|
|
143
|
+
const serialToDin = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const din of expansionDinsByBlock.flat()) {
|
|
145
|
+
const serial = serialFromDin(din);
|
|
146
|
+
if (serial) serialToDin.set(serial, din);
|
|
147
|
+
}
|
|
148
|
+
const matched = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const slot of slots) {
|
|
150
|
+
const serial = component_serial(components, slot);
|
|
151
|
+
const din = serial ? serialToDin.get(serial) : void 0;
|
|
152
|
+
if (din !== void 0 && !matched.has(din)) matched.set(din, slot);
|
|
153
|
+
}
|
|
154
|
+
const followers = followerSlots(components, powerwallCount, new Set(matched.values()), slots);
|
|
155
|
+
const assigned = /* @__PURE__ */ new Set([
|
|
156
|
+
0,
|
|
157
|
+
...followers,
|
|
158
|
+
...matched.values()
|
|
159
|
+
]);
|
|
160
|
+
const unclaimed = slots.filter((slot) => !assigned.has(slot));
|
|
161
|
+
let nextUnobserved = Math.max(powerwallCount, slots.length);
|
|
162
|
+
const expansionsByBlock = expansionDinsByBlock.map((dins) => {
|
|
163
|
+
const expansions = [];
|
|
164
|
+
for (const din of dins) {
|
|
165
|
+
const slot = matched.get(din) ?? unclaimed.shift();
|
|
166
|
+
if (slot !== void 0) expansions.push([din, slot]);
|
|
167
|
+
else if (slots.length === 0) expansions.push([din, nextUnobserved++]);
|
|
168
|
+
}
|
|
169
|
+
return expansions;
|
|
170
|
+
});
|
|
171
|
+
expansionsByBlock[0].push(...unclaimed.map((slot) => [null, slot]));
|
|
172
|
+
const unit = (role, block_index, din, slot) => ({
|
|
173
|
+
role,
|
|
174
|
+
block_index,
|
|
175
|
+
component_slot: slot,
|
|
176
|
+
din,
|
|
177
|
+
serial_number: component_serial(components, slot),
|
|
178
|
+
part_number: component_part_number(components, slot)
|
|
179
|
+
});
|
|
180
|
+
return blocks.flatMap((block, index) => [unit(index === 0 ? "leader" : "follower", index, unitDin(block), index === 0 ? 0 : followers[index - 1] ?? index), ...expansionsByBlock[index].map(([din, slot]) => unit("expansion", index, din, slot))]);
|
|
181
|
+
}
|
|
182
|
+
exports.battery_units = battery_units;
|
|
183
|
+
exports.component = component;
|
|
184
|
+
exports.component_float = component_float;
|
|
185
|
+
exports.component_part_number = component_part_number;
|
|
186
|
+
exports.component_serial = component_serial;
|
|
187
|
+
exports.component_signal = component_signal;
|
|
188
|
+
exports.pv_string_current = pv_string_current;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ComponentsPayload, ConfigPayload, StatusPayload } from "./models.cjs";
|
|
2
|
+
export type SignalValue = number | string | boolean;
|
|
3
|
+
export type BatteryRole = "leader" | "follower" | "expansion";
|
|
4
|
+
/**
|
|
5
|
+
* One battery unit: a Powerwall or an expansion.
|
|
6
|
+
*
|
|
7
|
+
* `din` comes from `config.json` or the status, and is `null` for an expansion
|
|
8
|
+
* found only in the components payload. `component_slot` is the index in each
|
|
9
|
+
* `components[<kind>]` list, which can be past the end of them while the gateway
|
|
10
|
+
* returns no BMS rows. `block_index` is the owning Powerwall's position in
|
|
11
|
+
* `battery_blocks`.
|
|
12
|
+
*/
|
|
13
|
+
export type BatteryUnit = {
|
|
14
|
+
role: BatteryRole;
|
|
15
|
+
block_index: number;
|
|
16
|
+
component_slot: number;
|
|
17
|
+
din: string | null;
|
|
18
|
+
serial_number: string | null;
|
|
19
|
+
part_number: string | null;
|
|
20
|
+
};
|
|
21
|
+
type Json = Record<string, any>;
|
|
22
|
+
/** The `kind` component at `slot`, or `null`. */
|
|
23
|
+
export declare function component(components: ComponentsPayload, kind: string, slot?: number): Json | null;
|
|
24
|
+
/** One signal of a component: its `value`, else `textValue`, else `boolValue`, or `null` when none is set. */
|
|
25
|
+
export declare function component_signal(components: ComponentsPayload, kind: string, name: string, slot?: number): SignalValue | null;
|
|
26
|
+
/** A numeric signal, or `null` when it is missing or not a number. */
|
|
27
|
+
export declare function component_float(components: ComponentsPayload, kind: string, name: string, slot?: number): number | null;
|
|
28
|
+
/** PCH PV string current in amps, for a `string` from `A` to `F`. An idle string's noise reads as 0. */
|
|
29
|
+
export declare function pv_string_current(components: ComponentsPayload, string: string, slot?: number): number | null;
|
|
30
|
+
/** The first non-empty serial number reported for `slot`. */
|
|
31
|
+
export declare function component_serial(components: ComponentsPayload, slot: number): string | null;
|
|
32
|
+
/** The first non-empty part number reported for `slot`. */
|
|
33
|
+
export declare function component_part_number(components: ComponentsPayload, slot: number): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Every battery unit of the site with its component slot, by block: each
|
|
36
|
+
* Powerwall, then its expansions.
|
|
37
|
+
*
|
|
38
|
+
* Powerwalls come from `battery_blocks` plus any extra DIN in the status; the
|
|
39
|
+
* first is the leader at slot 0. Expansions come from each block's
|
|
40
|
+
* `battery_expansions` and match a slot by serial number. A configured expansion
|
|
41
|
+
* with no real BMS row is dropped, and a real row no configured unit claims
|
|
42
|
+
* becomes an expansion of the leader with `din: null`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function battery_units(config: ConfigPayload, status: StatusPayload, components: ComponentsPayload): BatteryUnit[];
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ComponentsPayload, ConfigPayload, StatusPayload } from "./models.mjs";
|
|
2
|
+
export type SignalValue = number | string | boolean;
|
|
3
|
+
export type BatteryRole = "leader" | "follower" | "expansion";
|
|
4
|
+
/**
|
|
5
|
+
* One battery unit: a Powerwall or an expansion.
|
|
6
|
+
*
|
|
7
|
+
* `din` comes from `config.json` or the status, and is `null` for an expansion
|
|
8
|
+
* found only in the components payload. `component_slot` is the index in each
|
|
9
|
+
* `components[<kind>]` list, which can be past the end of them while the gateway
|
|
10
|
+
* returns no BMS rows. `block_index` is the owning Powerwall's position in
|
|
11
|
+
* `battery_blocks`.
|
|
12
|
+
*/
|
|
13
|
+
export type BatteryUnit = {
|
|
14
|
+
role: BatteryRole;
|
|
15
|
+
block_index: number;
|
|
16
|
+
component_slot: number;
|
|
17
|
+
din: string | null;
|
|
18
|
+
serial_number: string | null;
|
|
19
|
+
part_number: string | null;
|
|
20
|
+
};
|
|
21
|
+
type Json = Record<string, any>;
|
|
22
|
+
/** The `kind` component at `slot`, or `null`. */
|
|
23
|
+
export declare function component(components: ComponentsPayload, kind: string, slot?: number): Json | null;
|
|
24
|
+
/** One signal of a component: its `value`, else `textValue`, else `boolValue`, or `null` when none is set. */
|
|
25
|
+
export declare function component_signal(components: ComponentsPayload, kind: string, name: string, slot?: number): SignalValue | null;
|
|
26
|
+
/** A numeric signal, or `null` when it is missing or not a number. */
|
|
27
|
+
export declare function component_float(components: ComponentsPayload, kind: string, name: string, slot?: number): number | null;
|
|
28
|
+
/** PCH PV string current in amps, for a `string` from `A` to `F`. An idle string's noise reads as 0. */
|
|
29
|
+
export declare function pv_string_current(components: ComponentsPayload, string: string, slot?: number): number | null;
|
|
30
|
+
/** The first non-empty serial number reported for `slot`. */
|
|
31
|
+
export declare function component_serial(components: ComponentsPayload, slot: number): string | null;
|
|
32
|
+
/** The first non-empty part number reported for `slot`. */
|
|
33
|
+
export declare function component_part_number(components: ComponentsPayload, slot: number): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Every battery unit of the site with its component slot, by block: each
|
|
36
|
+
* Powerwall, then its expansions.
|
|
37
|
+
*
|
|
38
|
+
* Powerwalls come from `battery_blocks` plus any extra DIN in the status; the
|
|
39
|
+
* first is the leader at slot 0. Expansions come from each block's
|
|
40
|
+
* `battery_expansions` and match a slot by serial number. A configured expansion
|
|
41
|
+
* with no real BMS row is dropped, and a real row no configured unit claims
|
|
42
|
+
* becomes an expansion of the leader with `din: null`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function battery_units(config: ConfigPayload, status: StatusPayload, components: ComponentsPayload): BatteryUnit[];
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// src/powerwall/components.ts
|
|
2
|
+
/** Idle PCH string currents read as float noise around 1e-16 A. */
|
|
3
|
+
const PV_CURRENT_NOISE_FLOOR_A = .001;
|
|
4
|
+
const SERIAL_KINDS = [
|
|
5
|
+
"hvp",
|
|
6
|
+
"bms",
|
|
7
|
+
"pch",
|
|
8
|
+
"baggr"
|
|
9
|
+
];
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
/** Walk nested objects and lists; `undefined` if a step is missing. */
|
|
14
|
+
function path(data, ...keys) {
|
|
15
|
+
let value = data;
|
|
16
|
+
for (const key of keys) if (typeof key === "number") value = Array.isArray(value) ? value[key] : void 0;
|
|
17
|
+
else value = isRecord(value) ? value[key] : void 0;
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
/** The `kind` component at `slot`, or `null`. */
|
|
21
|
+
function component(components, kind, slot = 0) {
|
|
22
|
+
const item = path(components, "components", kind, slot);
|
|
23
|
+
return isRecord(item) ? item : null;
|
|
24
|
+
}
|
|
25
|
+
/** One signal of a component: its `value`, else `textValue`, else `boolValue`, or `null` when none is set. */
|
|
26
|
+
function component_signal(components, kind, name, slot = 0) {
|
|
27
|
+
const signals = component(components, kind, slot)?.signals;
|
|
28
|
+
const signal = (Array.isArray(signals) ? signals : []).find((item) => isRecord(item) && item.name === name);
|
|
29
|
+
return signal?.value ?? signal?.textValue ?? signal?.boolValue ?? null;
|
|
30
|
+
}
|
|
31
|
+
/** A numeric signal, or `null` when it is missing or not a number. */
|
|
32
|
+
function component_float(components, kind, name, slot = 0) {
|
|
33
|
+
const value = component_signal(components, kind, name, slot);
|
|
34
|
+
return typeof value === "number" ? value : null;
|
|
35
|
+
}
|
|
36
|
+
/** PCH PV string current in amps, for a `string` from `A` to `F`. An idle string's noise reads as 0. */
|
|
37
|
+
function pv_string_current(components, string, slot = 0) {
|
|
38
|
+
const value = component_float(components, "pch", `PCH_PvCurrent${string}`, slot);
|
|
39
|
+
return value !== null && Math.abs(value) < PV_CURRENT_NOISE_FLOOR_A ? 0 : value;
|
|
40
|
+
}
|
|
41
|
+
function firstAcrossKinds(components, slot, field) {
|
|
42
|
+
for (const kind of SERIAL_KINDS) {
|
|
43
|
+
const value = path(components, "components", kind, slot, field);
|
|
44
|
+
if (typeof value === "string" && value) return value;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** The first non-empty serial number reported for `slot`. */
|
|
49
|
+
function component_serial(components, slot) {
|
|
50
|
+
return firstAcrossKinds(components, slot, "serialNumber");
|
|
51
|
+
}
|
|
52
|
+
/** The first non-empty part number reported for `slot`. */
|
|
53
|
+
function component_part_number(components, slot) {
|
|
54
|
+
return firstAcrossKinds(components, slot, "partNumber");
|
|
55
|
+
}
|
|
56
|
+
function unitDin(item) {
|
|
57
|
+
for (const key of ["din", "vin"]) {
|
|
58
|
+
const value = item[key];
|
|
59
|
+
if (typeof value === "string" && value) return value;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function serialFromDin(din) {
|
|
64
|
+
return din.split("--").at(-1);
|
|
65
|
+
}
|
|
66
|
+
function bmsFull(components, slot) {
|
|
67
|
+
return component_float(components, "bms", "BMS_nominalFullPackEnergy", slot);
|
|
68
|
+
}
|
|
69
|
+
/** Component slots that look like real BMS modules. */
|
|
70
|
+
function bmsSlots(components) {
|
|
71
|
+
const items = path(components, "components", "bms");
|
|
72
|
+
if (!Array.isArray(items)) return [];
|
|
73
|
+
return items.flatMap((_, slot) => (bmsFull(components, slot) ?? 0) > 0 ? [slot] : []);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* BMS slots after dropping phantom expansion rows. A gateway can keep a
|
|
77
|
+
* registered but uninstalled expansion: a plausible capacity, no serial and
|
|
78
|
+
* near-zero energy. Such rows are dropped only when the system aggregate already
|
|
79
|
+
* matches the kept rows, so a real pack with a stale reading stays.
|
|
80
|
+
*/
|
|
81
|
+
function ghostFilteredBmsSlots(components, status) {
|
|
82
|
+
const slots = bmsSlots(components);
|
|
83
|
+
const aggregateWh = path(status, "control", "systemStatus", "nominalFullPackEnergyWh");
|
|
84
|
+
if (slots.length === 0 || typeof aggregateWh !== "number" || aggregateWh <= 0) return slots;
|
|
85
|
+
const aggregateKwh = aggregateWh / 1e3;
|
|
86
|
+
const ghosts = slots.filter((slot) => {
|
|
87
|
+
if (component_serial(components, slot)) return false;
|
|
88
|
+
const full = bmsFull(components, slot);
|
|
89
|
+
const remaining = component_float(components, "bms", "BMS_nominalEnergyRemaining", slot);
|
|
90
|
+
if (full === null || full <= 0 || remaining === null) return false;
|
|
91
|
+
return remaining < .5 || remaining / full < .05;
|
|
92
|
+
});
|
|
93
|
+
if (ghosts.length === 0) return slots;
|
|
94
|
+
const kept = slots.filter((slot) => !ghosts.includes(slot));
|
|
95
|
+
const keptKwh = kept.reduce((sum, slot) => sum + (bmsFull(components, slot) ?? 0), 0);
|
|
96
|
+
return keptKwh > 0 && Math.abs(aggregateKwh - keptKwh) / aggregateKwh < .1 ? kept : slots;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Choose the component slots of follower Powerwalls. A slot matched to a
|
|
100
|
+
* configured expansion is never chosen. Candidates with no serial win when
|
|
101
|
+
* there are enough of them; otherwise the last candidates are taken.
|
|
102
|
+
*/
|
|
103
|
+
function followerSlots(components, powerwallCount, expansionSlots, slots) {
|
|
104
|
+
const followerCount = Math.max(0, powerwallCount - 1);
|
|
105
|
+
if (followerCount === 0) return [];
|
|
106
|
+
if (slots.length === 0) return Array.from({ length: followerCount }, (_, index) => index + 1);
|
|
107
|
+
const candidates = slots.filter((slot) => slot !== 0 && !expansionSlots.has(slot));
|
|
108
|
+
const noSerial = candidates.filter((slot) => !component_serial(components, slot));
|
|
109
|
+
const chosen = (noSerial.length >= followerCount ? noSerial : candidates).slice(-followerCount);
|
|
110
|
+
for (let fallback = 1; chosen.length < followerCount; fallback++) if (!chosen.includes(fallback) && !expansionSlots.has(fallback)) chosen.push(fallback);
|
|
111
|
+
return chosen.sort((a, b) => a - b);
|
|
112
|
+
}
|
|
113
|
+
function statusBatteryDins(status) {
|
|
114
|
+
const blocks = path(status, "control", "batteryBlocks");
|
|
115
|
+
if (!Array.isArray(blocks)) return [];
|
|
116
|
+
return blocks.filter(isRecord).flatMap((block) => unitDin(block) ?? []);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Every battery unit of the site with its component slot, by block: each
|
|
120
|
+
* Powerwall, then its expansions.
|
|
121
|
+
*
|
|
122
|
+
* Powerwalls come from `battery_blocks` plus any extra DIN in the status; the
|
|
123
|
+
* first is the leader at slot 0. Expansions come from each block's
|
|
124
|
+
* `battery_expansions` and match a slot by serial number. A configured expansion
|
|
125
|
+
* with no real BMS row is dropped, and a real row no configured unit claims
|
|
126
|
+
* becomes an expansion of the leader with `din: null`.
|
|
127
|
+
*/
|
|
128
|
+
function battery_units(config, status, components) {
|
|
129
|
+
const configured = config.battery_blocks;
|
|
130
|
+
const blocks = Array.isArray(configured) ? configured.filter(isRecord) : [];
|
|
131
|
+
const known = new Set(blocks.map(unitDin));
|
|
132
|
+
for (const din of statusBatteryDins(status)) if (!known.has(din)) {
|
|
133
|
+
blocks.push({ din });
|
|
134
|
+
known.add(din);
|
|
135
|
+
}
|
|
136
|
+
if (blocks.length === 0) blocks.push({});
|
|
137
|
+
const powerwallCount = blocks.length;
|
|
138
|
+
const expansionDinsByBlock = blocks.map((block) => {
|
|
139
|
+
const expansions = block.battery_expansions;
|
|
140
|
+
return Array.isArray(expansions) ? expansions.filter(isRecord).flatMap((item) => unitDin(item) ?? []) : [];
|
|
141
|
+
});
|
|
142
|
+
const slots = ghostFilteredBmsSlots(components, status);
|
|
143
|
+
const serialToDin = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const din of expansionDinsByBlock.flat()) {
|
|
145
|
+
const serial = serialFromDin(din);
|
|
146
|
+
if (serial) serialToDin.set(serial, din);
|
|
147
|
+
}
|
|
148
|
+
const matched = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const slot of slots) {
|
|
150
|
+
const serial = component_serial(components, slot);
|
|
151
|
+
const din = serial ? serialToDin.get(serial) : void 0;
|
|
152
|
+
if (din !== void 0 && !matched.has(din)) matched.set(din, slot);
|
|
153
|
+
}
|
|
154
|
+
const followers = followerSlots(components, powerwallCount, new Set(matched.values()), slots);
|
|
155
|
+
const assigned = /* @__PURE__ */ new Set([
|
|
156
|
+
0,
|
|
157
|
+
...followers,
|
|
158
|
+
...matched.values()
|
|
159
|
+
]);
|
|
160
|
+
const unclaimed = slots.filter((slot) => !assigned.has(slot));
|
|
161
|
+
let nextUnobserved = Math.max(powerwallCount, slots.length);
|
|
162
|
+
const expansionsByBlock = expansionDinsByBlock.map((dins) => {
|
|
163
|
+
const expansions = [];
|
|
164
|
+
for (const din of dins) {
|
|
165
|
+
const slot = matched.get(din) ?? unclaimed.shift();
|
|
166
|
+
if (slot !== void 0) expansions.push([din, slot]);
|
|
167
|
+
else if (slots.length === 0) expansions.push([din, nextUnobserved++]);
|
|
168
|
+
}
|
|
169
|
+
return expansions;
|
|
170
|
+
});
|
|
171
|
+
expansionsByBlock[0].push(...unclaimed.map((slot) => [null, slot]));
|
|
172
|
+
const unit = (role, block_index, din, slot) => ({
|
|
173
|
+
role,
|
|
174
|
+
block_index,
|
|
175
|
+
component_slot: slot,
|
|
176
|
+
din,
|
|
177
|
+
serial_number: component_serial(components, slot),
|
|
178
|
+
part_number: component_part_number(components, slot)
|
|
179
|
+
});
|
|
180
|
+
return blocks.flatMap((block, index) => [unit(index === 0 ? "leader" : "follower", index, unitDin(block), index === 0 ? 0 : followers[index - 1] ?? index), ...expansionsByBlock[index].map(([din, slot]) => unit("expansion", index, din, slot))]);
|
|
181
|
+
}
|
|
182
|
+
export { battery_units, component, component_float, component_part_number, component_serial, component_signal, pv_string_current };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
const require_const = require("../const.cjs");
|
|
2
|
+
const require_energyisland = require("../energyisland.cjs");
|
|
3
|
+
const require_energyparsers = require("../energyparsers.cjs");
|
|
4
|
+
const require_client = require("./client.cjs");
|
|
5
|
+
// src/powerwall/energysite.ts
|
|
6
|
+
/** Best-effort cloud `grid_status` and `island_status` for each gateway grid string. */
|
|
7
|
+
const GRID_STATUS = {
|
|
8
|
+
SystemGridConnected: {
|
|
9
|
+
grid_status: "Active",
|
|
10
|
+
island_status: "on_grid"
|
|
11
|
+
},
|
|
12
|
+
SystemIslandedActive: {
|
|
13
|
+
grid_status: "Inactive",
|
|
14
|
+
island_status: "off_grid"
|
|
15
|
+
},
|
|
16
|
+
SystemIslandedReady: {
|
|
17
|
+
grid_status: "Inactive",
|
|
18
|
+
island_status: "off_grid"
|
|
19
|
+
},
|
|
20
|
+
SystemTransitionToGrid: {
|
|
21
|
+
grid_status: "Active",
|
|
22
|
+
island_status: "on_grid"
|
|
23
|
+
},
|
|
24
|
+
SystemTransitionToIsland: {
|
|
25
|
+
grid_status: "Inactive",
|
|
26
|
+
island_status: "off_grid"
|
|
27
|
+
},
|
|
28
|
+
SystemMicroGridFaulted: {
|
|
29
|
+
grid_status: "Inactive",
|
|
30
|
+
island_status: "off_grid"
|
|
31
|
+
},
|
|
32
|
+
SystemWaitForUser: {
|
|
33
|
+
grid_status: "Inactive",
|
|
34
|
+
island_status: "island_status_unknown"
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const GRID_STATUS_UNKNOWN = {
|
|
38
|
+
grid_status: "Unknown",
|
|
39
|
+
island_status: "island_status_unknown"
|
|
40
|
+
};
|
|
41
|
+
/** Grid-connected, the `mode` for `set_island_mode` that reconnects. */
|
|
42
|
+
const ISLAND_MODE_ON_GRID = require_const.EnergyIslandMode.ON_GRID;
|
|
43
|
+
/** Islanded, the `mode` for `set_island_mode` that goes off grid. */
|
|
44
|
+
const ISLAND_MODE_OFF_GRID = require_const.EnergyIslandMode.OFF_GRID;
|
|
45
|
+
function ok() {
|
|
46
|
+
return { response: {
|
|
47
|
+
code: 201,
|
|
48
|
+
message: "",
|
|
49
|
+
result: true
|
|
50
|
+
} };
|
|
51
|
+
}
|
|
52
|
+
function numberOrNull(value) {
|
|
53
|
+
return typeof value === "number" ? value : null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The cloud energy site's surface over a local `PowerwallClient`: the same method
|
|
57
|
+
* names, arguments and reply shapes, so a router can prefer the LAN and fall back
|
|
58
|
+
* to the cloud. It imports nothing from the router.
|
|
59
|
+
*
|
|
60
|
+
* What has no local mapping is absent rather than stubbed, so a router routes it
|
|
61
|
+
* to the cloud: `site_info`, `storm_mode`, the histories and the rest of Python's
|
|
62
|
+
* `NotImplementedError` group. `add_authorized_client` is one of those, because
|
|
63
|
+
* registration goes through the cloud.
|
|
64
|
+
*/
|
|
65
|
+
var PowerwallEnergySite = class {
|
|
66
|
+
#client;
|
|
67
|
+
/** @param client An existing client. This never opens its own transport. */
|
|
68
|
+
constructor(client) {
|
|
69
|
+
this.#client = client;
|
|
70
|
+
}
|
|
71
|
+
/** The wrapped client, for what has no cloud counterpart. */
|
|
72
|
+
get powerwall() {
|
|
73
|
+
return this.#client;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Set the operation mode.
|
|
77
|
+
* @param default_real_mode `self_consumption`, `autonomous` or `backup`.
|
|
78
|
+
*/
|
|
79
|
+
async operation(default_real_mode) {
|
|
80
|
+
await this.#client.set_operation_mode(default_real_mode);
|
|
81
|
+
return ok();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Set the backup reserve.
|
|
85
|
+
* @param backup_reserve_percent On the app's 0-100 scale, as the cloud takes it.
|
|
86
|
+
*/
|
|
87
|
+
async backup(backup_reserve_percent) {
|
|
88
|
+
await this.#client.set_backup_reserve(backup_reserve_percent);
|
|
89
|
+
return ok();
|
|
90
|
+
}
|
|
91
|
+
/** Set the grid-charging flag and export rule. Both in one call means one `config.json` write. */
|
|
92
|
+
async grid_import_export(disallow_charge_from_grid_with_solar_installed = null, customer_preferred_export_rule = null) {
|
|
93
|
+
await this.#client.set_grid_import_export({
|
|
94
|
+
customer_preferred_export_rule,
|
|
95
|
+
disallow_charge_from_grid_with_solar_installed
|
|
96
|
+
});
|
|
97
|
+
return ok();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Open or close the grid contactor. See `PowerwallClient.set_island_mode`:
|
|
101
|
+
* read `islanding.contactorClosed` afterwards.
|
|
102
|
+
* @param mode 6 islands and 1 reconnects. Anything else raises `TypeError`.
|
|
103
|
+
* @param force Defaults to true off grid and false otherwise, like the cloud.
|
|
104
|
+
*/
|
|
105
|
+
async set_island_mode(mode, force) {
|
|
106
|
+
require_energyisland.validate_island_mode(mode, force);
|
|
107
|
+
const off_grid = mode === require_const.EnergyIslandMode.OFF_GRID;
|
|
108
|
+
await this.#client.set_island_mode({
|
|
109
|
+
off_grid,
|
|
110
|
+
force: force ?? off_grid,
|
|
111
|
+
mode_override: mode
|
|
112
|
+
});
|
|
113
|
+
return ok();
|
|
114
|
+
}
|
|
115
|
+
/** Disconnect from the grid. */
|
|
116
|
+
async go_off_grid() {
|
|
117
|
+
await this.#client.go_off_grid();
|
|
118
|
+
return ok();
|
|
119
|
+
}
|
|
120
|
+
/** Reconnect to the grid. */
|
|
121
|
+
async reconnect_grid() {
|
|
122
|
+
await this.#client.reconnect_grid();
|
|
123
|
+
return ok();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Schedule a manual backup event. `start_time` and `priority` exist for
|
|
127
|
+
* signature parity and are not honoured: the event always starts now at
|
|
128
|
+
* maximum priority.
|
|
129
|
+
* @param duration_seconds Defaults to 7200.
|
|
130
|
+
*/
|
|
131
|
+
async schedule_backup_event(_start_time, duration_seconds, _priority) {
|
|
132
|
+
await this.#client.schedule_max_backup(duration_seconds ?? void 0);
|
|
133
|
+
return ok();
|
|
134
|
+
}
|
|
135
|
+
/** Cancel the active manual backup event. */
|
|
136
|
+
async cancel_backup_event() {
|
|
137
|
+
await this.#client.cancel_max_backup();
|
|
138
|
+
return ok();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Request a timed battery power setpoint.
|
|
142
|
+
* @param power_watts Positive discharges, negative charges.
|
|
143
|
+
* @param duration_seconds Whole seconds from 1 to 86400.
|
|
144
|
+
*/
|
|
145
|
+
async dispatch_battery_power(power_watts, duration_seconds) {
|
|
146
|
+
await this.#client.dispatch_battery_power(power_watts, duration_seconds);
|
|
147
|
+
return ok();
|
|
148
|
+
}
|
|
149
|
+
/** Release a `dispatch_battery_power` setpoint. */
|
|
150
|
+
async resume_battery_operation() {
|
|
151
|
+
await this.#client.resume_battery_operation();
|
|
152
|
+
return ok();
|
|
153
|
+
}
|
|
154
|
+
/** The active manual backup and scheduled events, in the local shape: the cloud's gRPC reply cannot be reproduced. */
|
|
155
|
+
async get_backup_events() {
|
|
156
|
+
return { response: await this.#client.get_backup_events() };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A cloud-shaped `live_status`, from three sequential reads. Keys with no local
|
|
160
|
+
* source are `null`, never guessed. A merge with the cloud fills them in.
|
|
161
|
+
*/
|
|
162
|
+
async live_status() {
|
|
163
|
+
const meters = await this.#client.get_meters_aggregates();
|
|
164
|
+
const status = await this.#client.get_status();
|
|
165
|
+
const gridStatus = await this.#client.get_grid_status();
|
|
166
|
+
const power = (location) => numberOrNull(meters[location]?.instant_power);
|
|
167
|
+
const systemStatus = status.control?.systemStatus;
|
|
168
|
+
return { response: {
|
|
169
|
+
solar_power: power("solar"),
|
|
170
|
+
energy_left: numberOrNull(systemStatus?.nominalEnergyRemainingWh),
|
|
171
|
+
total_pack_energy: numberOrNull(systemStatus?.nominalFullPackEnergyWh),
|
|
172
|
+
percentage_charged: require_client.battery_level(status),
|
|
173
|
+
backup_capable: null,
|
|
174
|
+
battery_power: power("battery"),
|
|
175
|
+
load_power: power("load"),
|
|
176
|
+
grid_power: power("site"),
|
|
177
|
+
grid_services_power: null,
|
|
178
|
+
generator_power: power("generator"),
|
|
179
|
+
...Object.hasOwn(GRID_STATUS, gridStatus) ? GRID_STATUS[gridStatus] : GRID_STATUS_UNKNOWN,
|
|
180
|
+
grid_services_active: null,
|
|
181
|
+
storm_mode_active: null,
|
|
182
|
+
timestamp: null,
|
|
183
|
+
wall_connectors: null
|
|
184
|
+
} };
|
|
185
|
+
}
|
|
186
|
+
/** The reserve (on the app's scale) and mode from `config.json`. A bare object, unlike the cloud's reads. */
|
|
187
|
+
async local_config() {
|
|
188
|
+
const config = await this.#client.get_config();
|
|
189
|
+
const result = {};
|
|
190
|
+
const reserve = config.site_info?.backup_reserve_percent;
|
|
191
|
+
if (typeof reserve === "number") result.backup_reserve_percent = require_client.raw_to_scaled_reserve(reserve);
|
|
192
|
+
if (config.default_real_mode !== void 0) result.default_real_mode = config.default_real_mode;
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
/** The paired keys in the gateway's raw form. Prefer `find_authorized_clients`. */
|
|
196
|
+
async list_authorized_clients() {
|
|
197
|
+
return { response: await this.#client.list_authorized_clients() };
|
|
198
|
+
}
|
|
199
|
+
/** The paired keys, in the same typed form as `TeslemetryEnergySite.find_authorized_clients`. */
|
|
200
|
+
async find_authorized_clients() {
|
|
201
|
+
const { response } = await this.list_authorized_clients();
|
|
202
|
+
return require_energyparsers.parse_authorized_clients(response);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Revoke a paired key. Any paired key can remove any other, and removing the
|
|
206
|
+
* signing key locks this client out.
|
|
207
|
+
* @param public_key Raw DER bytes, or base64 as `list_authorized_clients` reports it.
|
|
208
|
+
*/
|
|
209
|
+
async remove_authorized_client(public_key) {
|
|
210
|
+
await this.#client.remove_authorized_client(public_key);
|
|
211
|
+
return ok();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
exports.ISLAND_MODE_OFF_GRID = ISLAND_MODE_OFF_GRID;
|
|
215
|
+
exports.ISLAND_MODE_ON_GRID = ISLAND_MODE_ON_GRID;
|
|
216
|
+
exports.PowerwallEnergySite = PowerwallEnergySite;
|