tesla-fleet-api 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/README.md +284 -1
  2. package/dist/_virtual/_rolldown/runtime.cjs +32 -0
  3. package/dist/_virtual/_rolldown/runtime.mjs +12 -0
  4. package/dist/bluetooth/adapters/shared.cjs +38 -0
  5. package/dist/bluetooth/adapters/shared.mjs +36 -0
  6. package/dist/bluetooth/async.cjs +121 -0
  7. package/dist/bluetooth/async.d.cts +10 -0
  8. package/dist/bluetooth/async.d.mts +10 -0
  9. package/dist/bluetooth/async.mjs +116 -0
  10. package/dist/bluetooth/broadcast.cjs +153 -0
  11. package/dist/bluetooth/broadcast.d.cts +59 -0
  12. package/dist/bluetooth/broadcast.d.mts +59 -0
  13. package/dist/bluetooth/broadcast.mjs +151 -0
  14. package/dist/bluetooth/constants.cjs +99 -0
  15. package/dist/bluetooth/constants.d.cts +70 -0
  16. package/dist/bluetooth/constants.d.mts +70 -0
  17. package/dist/bluetooth/constants.mjs +72 -0
  18. package/dist/bluetooth/esphome.cjs +292 -0
  19. package/dist/bluetooth/esphome.d.cts +83 -0
  20. package/dist/bluetooth/esphome.d.mts +83 -0
  21. package/dist/bluetooth/esphome.mjs +291 -0
  22. package/dist/bluetooth/framing.cjs +125 -0
  23. package/dist/bluetooth/framing.d.cts +45 -0
  24. package/dist/bluetooth/framing.d.mts +45 -0
  25. package/dist/bluetooth/framing.mjs +122 -0
  26. package/dist/bluetooth/homey.cjs +155 -0
  27. package/dist/bluetooth/homey.d.cts +75 -0
  28. package/dist/bluetooth/homey.d.mts +75 -0
  29. package/dist/bluetooth/homey.mjs +150 -0
  30. package/dist/bluetooth/names.cjs +24 -0
  31. package/dist/bluetooth/names.d.cts +10 -0
  32. package/dist/bluetooth/names.d.mts +10 -0
  33. package/dist/bluetooth/names.mjs +20 -0
  34. package/dist/bluetooth/noble.cjs +178 -0
  35. package/dist/bluetooth/noble.d.cts +58 -0
  36. package/dist/bluetooth/noble.d.mts +58 -0
  37. package/dist/bluetooth/noble.mjs +177 -0
  38. package/dist/bluetooth/streamglue.cjs +157 -0
  39. package/dist/bluetooth/streamglue.d.cts +37 -0
  40. package/dist/bluetooth/streamglue.d.mts +37 -0
  41. package/dist/bluetooth/streamglue.mjs +154 -0
  42. package/dist/bluetooth/teslabluetooth.cjs +67 -0
  43. package/dist/bluetooth/teslabluetooth.d.cts +41 -0
  44. package/dist/bluetooth/teslabluetooth.d.mts +41 -0
  45. package/dist/bluetooth/teslabluetooth.mjs +66 -0
  46. package/dist/bluetooth/transport.cjs +10 -0
  47. package/dist/bluetooth/transport.d.cts +95 -0
  48. package/dist/bluetooth/transport.d.mts +95 -0
  49. package/dist/bluetooth/transport.mjs +10 -0
  50. package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
  51. package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
  52. package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
  53. package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
  54. package/dist/bluetooth/verify.cjs +93 -0
  55. package/dist/bluetooth/verify.d.cts +35 -0
  56. package/dist/bluetooth/verify.d.mts +35 -0
  57. package/dist/bluetooth/verify.mjs +91 -0
  58. package/dist/bluetooth.cjs +53 -0
  59. package/dist/bluetooth.d.cts +10 -0
  60. package/dist/bluetooth.d.mts +10 -0
  61. package/dist/bluetooth.mjs +9 -0
  62. package/dist/charging.cjs +53 -0
  63. package/dist/charging.d.cts +31 -0
  64. package/dist/charging.d.mts +30 -0
  65. package/dist/charging.mjs +53 -0
  66. package/dist/commands.cjs +1797 -0
  67. package/dist/commands.d.cts +881 -0
  68. package/dist/commands.d.mts +881 -0
  69. package/dist/commands.mjs +1791 -0
  70. package/dist/const.cjs +322 -0
  71. package/dist/const.d.cts +308 -0
  72. package/dist/const.d.mts +308 -0
  73. package/dist/const.mjs +282 -0
  74. package/dist/energy.cjs +142 -0
  75. package/dist/energy.d.cts +101 -0
  76. package/dist/energy.d.mts +100 -0
  77. package/dist/energy.mjs +142 -0
  78. package/dist/energyisland.cjs +38 -0
  79. package/dist/energyisland.mjs +37 -0
  80. package/dist/energyparsers.cjs +118 -0
  81. package/dist/energyparsers.d.cts +39 -0
  82. package/dist/energyparsers.d.mts +39 -0
  83. package/dist/energyparsers.mjs +116 -0
  84. package/dist/energyspecific.cjs +457 -0
  85. package/dist/energyspecific.d.cts +258 -0
  86. package/dist/energyspecific.d.mts +258 -0
  87. package/dist/energyspecific.mjs +451 -0
  88. package/dist/exceptions.cjs +1488 -0
  89. package/dist/exceptions.d.cts +1143 -0
  90. package/dist/exceptions.d.mts +1143 -0
  91. package/dist/exceptions.mjs +1325 -0
  92. package/dist/funnel.cjs +297 -0
  93. package/dist/funnel.d.cts +118 -0
  94. package/dist/funnel.d.mts +118 -0
  95. package/dist/funnel.mjs +292 -0
  96. package/dist/index.cjs +287 -0
  97. package/dist/index.d.cts +28 -0
  98. package/dist/index.d.mts +28 -0
  99. package/dist/index.mjs +27 -0
  100. package/dist/log.cjs +75 -0
  101. package/dist/log.d.cts +4 -0
  102. package/dist/log.d.mts +4 -0
  103. package/dist/log.mjs +71 -0
  104. package/dist/oauth.cjs +143 -0
  105. package/dist/oauth.d.cts +69 -0
  106. package/dist/oauth.d.mts +69 -0
  107. package/dist/oauth.mjs +143 -0
  108. package/dist/partner.cjs +32 -0
  109. package/dist/partner.d.cts +24 -0
  110. package/dist/partner.d.mts +23 -0
  111. package/dist/partner.mjs +32 -0
  112. package/dist/powerwall/client.cjs +818 -0
  113. package/dist/powerwall/client.d.cts +297 -0
  114. package/dist/powerwall/client.d.mts +297 -0
  115. package/dist/powerwall/client.mjs +807 -0
  116. package/dist/powerwall/components.cjs +188 -0
  117. package/dist/powerwall/components.d.cts +45 -0
  118. package/dist/powerwall/components.d.mts +45 -0
  119. package/dist/powerwall/components.mjs +182 -0
  120. package/dist/powerwall/energysite.cjs +216 -0
  121. package/dist/powerwall/energysite.d.cts +104 -0
  122. package/dist/powerwall/energysite.d.mts +104 -0
  123. package/dist/powerwall/energysite.mjs +214 -0
  124. package/dist/powerwall/exceptions.cjs +59 -0
  125. package/dist/powerwall/exceptions.d.cts +38 -0
  126. package/dist/powerwall/exceptions.d.mts +38 -0
  127. package/dist/powerwall/exceptions.mjs +51 -0
  128. package/dist/powerwall/gateway.cjs +102 -0
  129. package/dist/powerwall/gateway.d.cts +57 -0
  130. package/dist/powerwall/gateway.d.mts +57 -0
  131. package/dist/powerwall/gateway.mjs +101 -0
  132. package/dist/powerwall/merge.cjs +163 -0
  133. package/dist/powerwall/merge.d.cts +63 -0
  134. package/dist/powerwall/merge.d.mts +63 -0
  135. package/dist/powerwall/merge.mjs +159 -0
  136. package/dist/powerwall/models.d.cts +95 -0
  137. package/dist/powerwall/models.d.mts +95 -0
  138. package/dist/powerwall/queries.cjs +17 -0
  139. package/dist/powerwall/queries.mjs +10 -0
  140. package/dist/powerwall/transport.cjs +232 -0
  141. package/dist/powerwall/transport.mjs +227 -0
  142. package/dist/powerwall.cjs +47 -0
  143. package/dist/powerwall.d.cts +9 -0
  144. package/dist/powerwall.d.mts +9 -0
  145. package/dist/powerwall.mjs +8 -0
  146. package/dist/router/base.cjs +203 -0
  147. package/dist/router/base.d.cts +112 -0
  148. package/dist/router/base.d.mts +112 -0
  149. package/dist/router/base.mjs +200 -0
  150. package/dist/router/energysite.cjs +65 -0
  151. package/dist/router/energysite.d.cts +47 -0
  152. package/dist/router/energysite.d.mts +47 -0
  153. package/dist/router/energysite.mjs +60 -0
  154. package/dist/router.cjs +13 -0
  155. package/dist/router.d.cts +3 -0
  156. package/dist/router.d.mts +3 -0
  157. package/dist/router.mjs +3 -0
  158. package/dist/signing/crypto.cjs +59 -0
  159. package/dist/signing/crypto.mjs +52 -0
  160. package/dist/signing/energy.cjs +123 -0
  161. package/dist/signing/energy.d.cts +45 -0
  162. package/dist/signing/energy.d.mts +45 -0
  163. package/dist/signing/energy.mjs +120 -0
  164. package/dist/signing/jws.cjs +229 -0
  165. package/dist/signing/jws.d.cts +45 -0
  166. package/dist/signing/jws.d.mts +45 -0
  167. package/dist/signing/jws.mjs +211 -0
  168. package/dist/signing/keys.cjs +191 -0
  169. package/dist/signing/keys.d.cts +21 -0
  170. package/dist/signing/keys.d.mts +21 -0
  171. package/dist/signing/keys.mjs +182 -0
  172. package/dist/signing/session.cjs +186 -0
  173. package/dist/signing/session.d.cts +106 -0
  174. package/dist/signing/session.d.mts +106 -0
  175. package/dist/signing/session.mjs +184 -0
  176. package/dist/tariff.cjs +319 -0
  177. package/dist/tariff.d.cts +40 -0
  178. package/dist/tariff.d.mts +40 -0
  179. package/dist/tariff.mjs +317 -0
  180. package/dist/teslafleetapi.cjs +326 -0
  181. package/dist/teslafleetapi.d.cts +160 -0
  182. package/dist/teslafleetapi.d.mts +160 -0
  183. package/dist/teslafleetapi.mjs +326 -0
  184. package/dist/teslemetry.cjs +183 -0
  185. package/dist/teslemetry.d.cts +113 -0
  186. package/dist/teslemetry.d.mts +113 -0
  187. package/dist/teslemetry.mjs +181 -0
  188. package/dist/teslemetryenergy.cjs +287 -0
  189. package/dist/teslemetryenergy.d.cts +154 -0
  190. package/dist/teslemetryenergy.d.mts +154 -0
  191. package/dist/teslemetryenergy.mjs +282 -0
  192. package/dist/teslemetryvehicle.cjs +407 -0
  193. package/dist/teslemetryvehicle.d.cts +238 -0
  194. package/dist/teslemetryvehicle.d.mts +238 -0
  195. package/dist/teslemetryvehicle.mjs +405 -0
  196. package/dist/tessie.cjs +71 -0
  197. package/dist/tessie.d.cts +69 -0
  198. package/dist/tessie.d.mts +69 -0
  199. package/dist/tessie.mjs +71 -0
  200. package/dist/tessievehicle.cjs +514 -0
  201. package/dist/tessievehicle.d.cts +333 -0
  202. package/dist/tessievehicle.d.mts +333 -0
  203. package/dist/tessievehicle.mjs +513 -0
  204. package/dist/types/commands.d.cts +28 -0
  205. package/dist/types/commands.d.mts +28 -0
  206. package/dist/types/live_status.d.cts +30 -0
  207. package/dist/types/live_status.d.mts +30 -0
  208. package/dist/types/products.d.cts +58 -0
  209. package/dist/types/products.d.mts +58 -0
  210. package/dist/types/responses.d.cts +13 -0
  211. package/dist/types/responses.d.mts +13 -0
  212. package/dist/types/site_info.d.cts +154 -0
  213. package/dist/types/site_info.d.mts +154 -0
  214. package/dist/types/vehicle.d.cts +33 -0
  215. package/dist/types/vehicle.d.mts +33 -0
  216. package/dist/types/vehicle_data.d.cts +278 -0
  217. package/dist/types/vehicle_data.d.mts +278 -0
  218. package/dist/types.cjs +0 -0
  219. package/dist/types.d.cts +9 -0
  220. package/dist/types.d.mts +9 -0
  221. package/dist/types.mjs +1 -0
  222. package/dist/user.cjs +28 -0
  223. package/dist/user.d.cts +17 -0
  224. package/dist/user.d.mts +16 -0
  225. package/dist/user.mjs +28 -0
  226. package/dist/util.cjs +41 -0
  227. package/dist/util.d.cts +18 -0
  228. package/dist/util.d.mts +18 -0
  229. package/dist/util.mjs +40 -0
  230. package/dist/vehicle.cjs +847 -0
  231. package/dist/vehicle.d.cts +566 -0
  232. package/dist/vehicle.d.mts +565 -0
  233. package/dist/vehicle.mjs +847 -0
  234. package/dist/vehiclesigned.cjs +35 -0
  235. package/dist/vehiclesigned.d.cts +23 -0
  236. package/dist/vehiclesigned.d.mts +22 -0
  237. package/dist/vehiclesigned.mjs +35 -0
  238. package/dist/vehiclespecific.cjs +715 -0
  239. package/dist/vehiclespecific.d.cts +497 -0
  240. package/dist/vehiclespecific.d.mts +496 -0
  241. package/dist/vehiclespecific.mjs +715 -0
  242. package/package.json +178 -38
  243. package/dist/charging.d.ts +0 -30
  244. package/dist/charging.js +0 -51
  245. package/dist/charging.js.map +0 -1
  246. package/dist/commands.d.ts +0 -352
  247. package/dist/commands.js +0 -875
  248. package/dist/commands.js.map +0 -1
  249. package/dist/energy.d.ts +0 -94
  250. package/dist/energy.js +0 -125
  251. package/dist/energy.js.map +0 -1
  252. package/dist/energyspecific.d.ts +0 -77
  253. package/dist/energyspecific.js +0 -97
  254. package/dist/energyspecific.js.map +0 -1
  255. package/dist/helpers.d.ts +0 -2
  256. package/dist/helpers.js +0 -10
  257. package/dist/helpers.js.map +0 -1
  258. package/dist/index.d.ts +0 -15
  259. package/dist/index.js +0 -15
  260. package/dist/index.js.map +0 -1
  261. package/dist/partner.d.ts +0 -23
  262. package/dist/partner.js +0 -31
  263. package/dist/partner.js.map +0 -1
  264. package/dist/signing/crypto.d.ts +0 -10
  265. package/dist/signing/crypto.js +0 -26
  266. package/dist/signing/crypto.js.map +0 -1
  267. package/dist/signing/errors.d.ts +0 -29
  268. package/dist/signing/errors.js +0 -40
  269. package/dist/signing/errors.js.map +0 -1
  270. package/dist/signing/session.d.ts +0 -69
  271. package/dist/signing/session.js +0 -102
  272. package/dist/signing/session.js.map +0 -1
  273. package/dist/tariff.d.ts +0 -29
  274. package/dist/tariff.js +0 -302
  275. package/dist/tariff.js.map +0 -1
  276. package/dist/teslafleetapi.d.ts +0 -58
  277. package/dist/teslafleetapi.js +0 -120
  278. package/dist/teslafleetapi.js.map +0 -1
  279. package/dist/teslemetry.d.ts +0 -29
  280. package/dist/teslemetry.js +0 -39
  281. package/dist/teslemetry.js.map +0 -1
  282. package/dist/tessie.d.ts +0 -23
  283. package/dist/tessie.js +0 -28
  284. package/dist/tessie.js.map +0 -1
  285. package/dist/types/commands.d.ts +0 -44
  286. package/dist/types/commands.js +0 -11
  287. package/dist/types/commands.js.map +0 -1
  288. package/dist/types/live_status.d.ts +0 -26
  289. package/dist/types/live_status.js +0 -2
  290. package/dist/types/live_status.js.map +0 -1
  291. package/dist/types/products.d.ts +0 -54
  292. package/dist/types/products.js +0 -2
  293. package/dist/types/products.js.map +0 -1
  294. package/dist/types/responses.d.ts +0 -5
  295. package/dist/types/responses.js +0 -2
  296. package/dist/types/responses.js.map +0 -1
  297. package/dist/types/site_info.d.ts +0 -150
  298. package/dist/types/site_info.js +0 -2
  299. package/dist/types/site_info.js.map +0 -1
  300. package/dist/types/vehicle.d.ts +0 -23
  301. package/dist/types/vehicle.js +0 -10
  302. package/dist/types/vehicle.js.map +0 -1
  303. package/dist/types/vehicle_data.d.ts +0 -274
  304. package/dist/types/vehicle_data.js +0 -2
  305. package/dist/types/vehicle_data.js.map +0 -1
  306. package/dist/user.d.ts +0 -16
  307. package/dist/user.js +0 -26
  308. package/dist/user.js.map +0 -1
  309. package/dist/vehicle.d.ts +0 -548
  310. package/dist/vehicle.js +0 -774
  311. package/dist/vehicle.js.map +0 -1
  312. package/dist/vehiclesigned.d.ts +0 -13
  313. package/dist/vehiclesigned.js +0 -19
  314. package/dist/vehiclesigned.js.map +0 -1
  315. package/dist/vehiclespecific.d.ts +0 -432
  316. package/dist/vehiclespecific.js +0 -601
  317. package/dist/vehiclespecific.js.map +0 -1
  318. package/src/charging.ts +0 -66
  319. package/src/commands.ts +0 -1134
  320. package/src/energy.ts +0 -217
  321. package/src/energyspecific.ts +0 -116
  322. package/src/helpers.ts +0 -11
  323. package/src/index.ts +0 -21
  324. package/src/partner.ts +0 -37
  325. package/src/signing/crypto.ts +0 -29
  326. package/src/signing/errors.ts +0 -50
  327. package/src/signing/session.ts +0 -132
  328. package/src/tariff.ts +0 -375
  329. package/src/teslafleetapi.ts +0 -182
  330. package/src/teslemetry.ts +0 -54
  331. package/src/tessie.ts +0 -42
  332. package/src/types/commands.ts +0 -62
  333. package/src/types/live_status.ts +0 -27
  334. package/src/types/products.ts +0 -61
  335. package/src/types/responses.ts +0 -1
  336. package/src/types/site_info.ts +0 -159
  337. package/src/types/vehicle.ts +0 -33
  338. package/src/types/vehicle_data.ts +0 -286
  339. package/src/user.ts +0 -35
  340. package/src/vehicle.ts +0 -886
  341. package/src/vehiclesigned.ts +0 -20
  342. package/src/vehiclespecific.ts +0 -818
@@ -0,0 +1,818 @@
1
+ const require_exceptions = require("./exceptions.cjs");
2
+ const require_energyisland = require("../energyisland.cjs");
3
+ const require_energyspecific = require("../energyspecific.cjs");
4
+ const require_queries = require("./queries.cjs");
5
+ const require_transport = require("./transport.cjs");
6
+ let _teslemetry_tesla_protocol_energy_device_transport = require("@teslemetry/tesla-protocol/energy_device/transport");
7
+ let _teslemetry_tesla_protocol_energy_device_teg_api = require("@teslemetry/tesla-protocol/energy_device/teg_api");
8
+ let _teslemetry_tesla_protocol_energy_device_authorization_types = require("@teslemetry/tesla-protocol/energy_device/authorization_types");
9
+ let _teslemetry_tesla_protocol_energy_device_common_api = require("@teslemetry/tesla-protocol/energy_device/common_api");
10
+ let _teslemetry_tesla_protocol_energy_device_filestore_api = require("@teslemetry/tesla-protocol/energy_device/filestore_api");
11
+ let _teslemetry_tesla_protocol_energy_device_graphql_api = require("@teslemetry/tesla-protocol/energy_device/graphql_api");
12
+ let _teslemetry_tesla_protocol_energy_device_local_auth_api = require("@teslemetry/tesla-protocol/energy_device/local_auth_api");
13
+ // src/powerwall/client.ts
14
+ /** The gateway's own Wi-Fi access point. */
15
+ const DEFAULT_GATEWAY_HOST = "192.168.91.1";
16
+ /** Values of `default_real_mode`. Each sticks and reads back verbatim on a Powerwall 3. */
17
+ const OPERATION_MODES = [
18
+ "self_consumption",
19
+ "autonomous",
20
+ "backup"
21
+ ];
22
+ /** Values of `site_info.customer_preferred_export_rule`. Each sticks and reads back verbatim on a Powerwall 3. */
23
+ const GRID_EXPORT_RULES = [
24
+ "battery_ok",
25
+ "pv_only",
26
+ "never"
27
+ ];
28
+ /** The default length of a manual backup event, and the shortest the gateway takes. */
29
+ const DEFAULT_BACKUP_DURATION_SECONDS = 7200;
30
+ const MIN_BACKUP_DURATION_SECONDS = 60;
31
+ /** `2^64 - 1`, which pre-empts any other event. A string because it does not fit a JS number. */
32
+ const MAX_PRIORITY = "18446744073709551615";
33
+ const MAX_UINT32 = 2 ** 32 - 1;
34
+ /** `google.rpc.Code.UNIMPLEMENTED`, for a command the firmware does not implement. */
35
+ const STATUS_UNIMPLEMENTED = 12;
36
+ /** `google.rpc.Code.NOT_FOUND`: a cancel with no active event to cancel. */
37
+ const STATUS_NOT_FOUND = 5;
38
+ /** The bottom 5% of both scales is an inaccessible buffer: `raw = scaled * 0.95 + 5`. */
39
+ const SCALE = .95;
40
+ const OFFSET = 5;
41
+ const CONFIG_FILE = "config.json";
42
+ function isRecord(value) {
43
+ return typeof value === "object" && value !== null && !Array.isArray(value);
44
+ }
45
+ /** Walk nested objects; `undefined` if any step is missing. */
46
+ function lookup(data, ...keys) {
47
+ return keys.reduce((value, key) => isRecord(value) ? value[key] : void 0, data);
48
+ }
49
+ /** Round to 4 decimal places. Within 0.0001 of Python's `round` on exact binary ties. */
50
+ function round4(value) {
51
+ return Number(value.toFixed(4));
52
+ }
53
+ /**
54
+ * Enum name with `prefix` removed. A firmware newer than this package can
55
+ * report a number with no name: that comes back as the number, and never throws.
56
+ */
57
+ function enumSuffix(toJSON, value, prefix) {
58
+ const name = toJSON(value);
59
+ return name === "UNRECOGNIZED" ? value : name.slice(prefix.length);
60
+ }
61
+ function seconds(date) {
62
+ return Math.floor(date.getTime() / 1e3);
63
+ }
64
+ function base64(bytes) {
65
+ return Buffer.from(bytes).toString("base64");
66
+ }
67
+ function parseJsonObject(text, what) {
68
+ let data;
69
+ try {
70
+ data = JSON.parse(text);
71
+ } catch (error) {
72
+ throw new require_exceptions.PowerwallProtocolError(`${what} payload is not valid JSON`, { cause: error });
73
+ }
74
+ if (!isRecord(data)) throw new require_exceptions.PowerwallProtocolError(`${what} payload is not an object`);
75
+ return data;
76
+ }
77
+ function decodeConfig(blob) {
78
+ let text;
79
+ try {
80
+ text = new TextDecoder("utf-8", { fatal: true }).decode(blob);
81
+ } catch (error) {
82
+ throw new require_exceptions.PowerwallProtocolError("config.json blob is not UTF-8", { cause: error });
83
+ }
84
+ return parseJsonObject(text, "config.json");
85
+ }
86
+ /** Set a dotted path in `config`. A missing or non-object step becomes `{}`. */
87
+ function applyDottedUpdate(config, dottedPath, value) {
88
+ const keys = dottedPath.split(".");
89
+ const last = keys.pop();
90
+ let target = config;
91
+ for (const key of keys) {
92
+ const existing = Object.hasOwn(target, key) ? target[key] : void 0;
93
+ target = isRecord(existing) ? existing : target[key] = {};
94
+ }
95
+ target[last] = value;
96
+ }
97
+ function requireNonEmptyString(value, name) {
98
+ if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
99
+ if (!value) throw new RangeError(`${name} must not be empty`);
100
+ }
101
+ function requireNonNegative(value, name) {
102
+ if (typeof value !== "number") throw new TypeError(`${name} must be a number`);
103
+ if (!Number.isFinite(value) || value < 0) throw new RangeError(`${name} must be a finite, non-negative number`);
104
+ }
105
+ /** `JSON.stringify` writes a non-finite number as `null`, so reject one at any depth rather than store it. */
106
+ function requireFiniteNumbers(value, name) {
107
+ if (typeof value === "number" && !Number.isFinite(value)) throw new RangeError(`${name} must be finite`);
108
+ if (typeof value === "object" && value !== null) for (const [key, item] of Object.entries(value)) requireFiniteNumbers(item, `${name}.${key}`);
109
+ }
110
+ function requireIntegerInRange(value, name, minimum, maximum) {
111
+ if (typeof value !== "number") throw new TypeError(`${name} must be a number`);
112
+ if (!Number.isInteger(value) || value < minimum || value > maximum) throw new RangeError(`${name} must be an integer from ${minimum} to ${maximum}`);
113
+ }
114
+ function decodeEnvelope(inner, what) {
115
+ try {
116
+ return _teslemetry_tesla_protocol_energy_device_transport.MessageEnvelope.decode(inner);
117
+ } catch (error) {
118
+ throw new require_exceptions.PowerwallProtocolError(`Malformed ${what} response`, { cause: error });
119
+ }
120
+ }
121
+ /** True when a reply's categories, such as `teg`, hold no message. The header says nothing about the command. */
122
+ function isEmptyReply(reply) {
123
+ const { deliveryChannel: _channel, sender: _sender, recipient: _recipient, ...categories } = reply;
124
+ return Object.values(categories).every((category) => category === void 0 || isRecord(category) && Object.values(category).every((item) => item === void 0));
125
+ }
126
+ /** Header for the `filestore`, `teg` and `authorization` messages. */
127
+ function commandHeader(din) {
128
+ return {
129
+ deliveryChannel: _teslemetry_tesla_protocol_energy_device_transport.DeliveryChannel.DELIVERY_CHANNEL_HERMES_COMMAND,
130
+ sender: { authorizedClient: _teslemetry_tesla_protocol_energy_device_authorization_types.AuthorizedClientType.AUTHORIZED_CLIENT_TYPE_CUSTOMER_MOBILE_APP },
131
+ recipient: { din }
132
+ };
133
+ }
134
+ /** Header for GraphQL and system-info reads, sent as a local installer. */
135
+ function localHeader(din) {
136
+ return {
137
+ deliveryChannel: _teslemetry_tesla_protocol_energy_device_transport.DeliveryChannel.DELIVERY_CHANNEL_LOCAL_HTTPS,
138
+ sender: { local: _teslemetry_tesla_protocol_energy_device_local_auth_api.LocalParticipant.LOCAL_PARTICIPANT_INSTALLER },
139
+ recipient: { din }
140
+ };
141
+ }
142
+ function programBoundary(boundary) {
143
+ if (boundary === void 0) return null;
144
+ return {
145
+ static_condition: enumSuffix(_teslemetry_tesla_protocol_energy_device_teg_api.controllableDeviceProgramScheduleBoundary_StaticConditionToJSON, boundary.staticCondition, "STATIC_CONDITION_"),
146
+ timestamp: boundary.timestamp ? seconds(boundary.timestamp) : null,
147
+ islanding_state: enumSuffix(_teslemetry_tesla_protocol_energy_device_teg_api.controllableDeviceProgramScheduleBoundary_IslandingStateToJSON, boundary.islandingState, "ISLANDING_STATE_"),
148
+ state_of_energy: enumSuffix(_teslemetry_tesla_protocol_energy_device_teg_api.controllableDeviceProgramScheduleBoundary_StateOfEnergyToJSON, boundary.stateOfEnergy, "STATE_OF_ENERGY_")
149
+ };
150
+ }
151
+ function controllableProgram(program) {
152
+ return {
153
+ key: {
154
+ device_id: program.key?.deviceId ?? "",
155
+ priority: enumSuffix(_teslemetry_tesla_protocol_energy_device_teg_api.controllableDeviceProgramKey_PriorityToJSON, program.key?.priority ?? 0, "PRIORITY_")
156
+ },
157
+ settings: { contactor_state: enumSuffix(_teslemetry_tesla_protocol_energy_device_teg_api.controllableDeviceProgramSettings_ContactorStateToJSON, program.settings?.contactorState ?? 0, "CONTACTOR_STATE_") },
158
+ schedule: {
159
+ start: programBoundary(program.schedule?.start),
160
+ end: programBoundary(program.schedule?.end),
161
+ expiry: programBoundary(program.schedule?.expiry)
162
+ }
163
+ };
164
+ }
165
+ function authorizedClient(record) {
166
+ return {
167
+ public_key: base64(record.publicKey),
168
+ state: enumSuffix(_teslemetry_tesla_protocol_energy_device_authorization_types.authorizedStateToJSON, record.state, "AUTHORIZED_STATE_"),
169
+ type: enumSuffix(_teslemetry_tesla_protocol_energy_device_authorization_types.authorizedClientTypeToJSON, record.type, "AUTHORIZED_CLIENT_TYPE_"),
170
+ description: record.description,
171
+ key_type: enumSuffix(_teslemetry_tesla_protocol_energy_device_authorization_types.authorizedKeyTypeToJSON, record.keyType, "AUTHORIZED_KEY_TYPE_"),
172
+ roles: record.roles.map((role) => enumSuffix(_teslemetry_tesla_protocol_energy_device_authorization_types.authorizationRoleToJSON, role, "AUTHORIZATION_ROLE_")),
173
+ verification: enumSuffix(_teslemetry_tesla_protocol_energy_device_authorization_types.authorizedVerificationTypeToJSON, record.verification, "AUTHORIZED_VERIFICATION_TYPE_"),
174
+ added_time: record.addedTime ? seconds(record.addedTime) : null,
175
+ identifier: record.identifier ?? null,
176
+ authorized_by_public_key: record.authorizedByPublicKey ? base64(record.authorizedByPublicKey) : null
177
+ };
178
+ }
179
+ /**
180
+ * Client for a gateway on the LAN, over `/tedapi/v1r` and its REST reads.
181
+ *
182
+ * The connection is not authenticated: like Python, this accepts any
183
+ * certificate, and replies carry no signature. A device on the LAN that can
184
+ * intercept traffic can read the password and forge readings, but it cannot
185
+ * forge a signed command.
186
+ */
187
+ var PowerwallClient = class PowerwallClient {
188
+ #transport;
189
+ #logger;
190
+ #expectedDin;
191
+ #din = null;
192
+ #connecting = null;
193
+ #configWrites = Promise.resolve();
194
+ #savedRealMode = null;
195
+ #savedReservePercent = null;
196
+ #curtailmentActive = false;
197
+ /** @throws PowerwallAuthenticationError for a bad PEM or a key that is not RSA. */
198
+ constructor(options, _legacyGateway = false) {
199
+ this.#transport = new require_transport.V1rTransport({
200
+ host: options.host ?? "192.168.91.1",
201
+ password: options.gatewayPassword,
202
+ rsaPrivateKeyPem: options.rsaPrivateKeyPem,
203
+ timeoutMs: options.timeoutMs,
204
+ legacyGateway: _legacyGateway
205
+ });
206
+ this.#logger = options.logger;
207
+ this.#expectedDin = options.expectedDin;
208
+ }
209
+ /**
210
+ * A client for an early Gateway 1 unit, which rejects the last-5 login and
211
+ * needs the full `gatewayPassword`. Everything else is the same.
212
+ */
213
+ static legacy_gateway(options) {
214
+ return new PowerwallClient(options, true);
215
+ }
216
+ get host() {
217
+ return this.#transport.host;
218
+ }
219
+ /** The gateway DIN, or `null` until `connect` succeeds. */
220
+ get din() {
221
+ return this.#din;
222
+ }
223
+ /** True between `curtail` and `restore_from_curtailment`. */
224
+ get curtailment_active() {
225
+ return this.#curtailmentActive;
226
+ }
227
+ /** Log in and fetch the gateway DIN. Idempotent, and concurrent callers share one attempt. */
228
+ connect() {
229
+ if (this.#din !== null) return Promise.resolve(this.#din);
230
+ this.#connecting ??= (async () => {
231
+ await this.#transport.login();
232
+ const din = await this.#transport.fetch_din();
233
+ if (this.#expectedDin && din !== this.#expectedDin) throw new require_exceptions.PowerwallAuthenticationError("The gateway at this address is not the expected one");
234
+ this.#din = din;
235
+ return din;
236
+ })().finally(() => {
237
+ this.#connecting = null;
238
+ });
239
+ return this.#connecting;
240
+ }
241
+ /** Destroy the idle sockets. */
242
+ close() {
243
+ this.#transport.close();
244
+ }
245
+ /** The cached DIN, connecting first if needed. */
246
+ get_din() {
247
+ return this.connect();
248
+ }
249
+ /** `/api/meters/aggregates`: instantaneous power per location. */
250
+ async get_meters_aggregates() {
251
+ await this.connect();
252
+ const data = await this.#transport.api_get("/api/meters/aggregates");
253
+ if (!isRecord(data)) throw new require_exceptions.PowerwallProtocolError("meters/aggregates payload is not an object");
254
+ return data;
255
+ }
256
+ /** The raw state of charge, where the bottom 5% is an inaccessible buffer. It reads higher than the Tesla app. */
257
+ async get_battery_soe_raw() {
258
+ await this.connect();
259
+ const data = await this.#transport.api_get("/api/system_status/soe");
260
+ const percentage = isRecord(data) ? Number(data.percentage ?? NaN) : NaN;
261
+ if (Number.isNaN(percentage)) throw new require_exceptions.PowerwallProtocolError("Unexpected SoE payload");
262
+ return percentage;
263
+ }
264
+ /** The state of charge the Tesla app and Fleet API show. */
265
+ async get_battery_soe() {
266
+ return raw_to_scaled_soc(await this.get_battery_soe_raw());
267
+ }
268
+ /** The gateway grid status string, such as `SystemGridConnected`. */
269
+ async get_grid_status() {
270
+ await this.connect();
271
+ const data = await this.#transport.api_get("/api/system_status/grid_status");
272
+ if (!isRecord(data) || !("grid_status" in data)) throw new require_exceptions.PowerwallProtocolError("Unexpected grid_status payload");
273
+ return String(data.grid_status);
274
+ }
275
+ /** The gateway's `config.json`. `battery_blocks` is always present. */
276
+ async get_config() {
277
+ const din = await this.connect();
278
+ const { blob } = await this._read_config(din);
279
+ const config = decodeConfig(blob);
280
+ config.battery_blocks ??= [];
281
+ return config;
282
+ }
283
+ /** The gateway status, from the narrow DeviceControllerQuery. */
284
+ async get_status() {
285
+ return parseJsonObject(await this._query_graphql(require_queries.STATUS_QUERY, require_queries.STATUS_CODE, "{}"), "status");
286
+ }
287
+ /** The extended DeviceControllerQuery payload. */
288
+ async get_device_controller() {
289
+ return parseJsonObject(await this._query_graphql(require_queries.CONTROLLER_QUERY, require_queries.CONTROLLER_CODE, require_queries.CONTROLLER_VARIABLES), "device_controller");
290
+ }
291
+ /** Powerwall 3 component data: `pch`, `pws`, `bms`, `hvp` and `baggr`. */
292
+ async get_components() {
293
+ return parseJsonObject(await this._query_graphql(require_queries.COMPONENTS_QUERY, require_queries.COMPONENTS_CODE, require_queries.COMPONENTS_VARIABLES), "components");
294
+ }
295
+ /** Gateway firmware details. Python's `five` and `six` keys are `update_status` and `device_type` here. */
296
+ async get_firmware_details() {
297
+ const din = await this.connect();
298
+ const info = (await this._post(_teslemetry_tesla_protocol_energy_device_transport.MessageEnvelope.create({
299
+ ...localHeader(din),
300
+ common: { getSystemInfoRequest: {} }
301
+ }), din, "firmware")).common?.getSystemInfoResponse ?? _teslemetry_tesla_protocol_energy_device_common_api.CommonAPIGetSystemInfoResponse.create();
302
+ return { system: {
303
+ gateway: {
304
+ partNumber: info.deviceId?.partNumber ?? "",
305
+ serialNumber: info.deviceId?.serialNumber ?? ""
306
+ },
307
+ din: info.din,
308
+ version: {
309
+ text: info.firmwareVersion?.version ?? "",
310
+ githash: info.firmwareVersion?.githash ?? /* @__PURE__ */ new Uint8Array()
311
+ },
312
+ update_status: info.systemUpdate?.updateStatus ?? 0,
313
+ device_type: info.deviceType,
314
+ wireless: { device: [] }
315
+ } };
316
+ }
317
+ /** The active manual backup and any scheduled backup events. Times are epoch seconds. */
318
+ async get_backup_events() {
319
+ const din = await this.connect();
320
+ const events = (await this._send_command_request(din, { teg: { getBackupEventsRequest: {} } }, (reply) => reply.teg?.getBackupEventsResponse, "get_backup_events_request")).teg?.getBackupEventsResponse;
321
+ const manual = events?.manualBackupEvent;
322
+ const start_time = manual?.schedulingInfo?.startTime ? seconds(manual.schedulingInfo.startTime) : 0;
323
+ const duration_seconds = manual?.schedulingInfo?.durationSeconds ?? 0;
324
+ const end_time = start_time + duration_seconds;
325
+ return {
326
+ manual_backup: manual ? {
327
+ start_time,
328
+ duration_seconds,
329
+ end_time,
330
+ active: Math.floor(Date.now() / 1e3) < end_time,
331
+ priority: manual.schedulingInfo?.priority ?? "0"
332
+ } : null,
333
+ backup_events: (events?.backupEvents ?? []).map((event) => ({
334
+ id: event.id,
335
+ name: event.name,
336
+ start_time: event.schedulingInfo?.startTime ? seconds(event.schedulingInfo.startTime) : 0,
337
+ duration_seconds: event.schedulingInfo?.durationSeconds ?? 0,
338
+ priority: event.schedulingInfo?.priority ?? "0"
339
+ }))
340
+ };
341
+ }
342
+ /**
343
+ * The battery calibration delay counters.
344
+ * @throws PowerwallNotSupportedError on firmware without this command. Powerwall 3 firmware 26.26.4 is one.
345
+ */
346
+ async get_battery_calibration_delay_status() {
347
+ const din = await this.connect();
348
+ const status = (await this._send_command_request(din, { teg: { getDelayBatteryCalibrationStatusRequest: {} } }, (reply) => reply.teg?.getDelayBatteryCalibrationStatusResponse, "get_delay_battery_calibration_status_request")).teg?.getDelayBatteryCalibrationStatusResponse;
349
+ return {
350
+ calibration_delays_left: status?.calibrationDelaysLeft ?? 0,
351
+ calibration_delay_remaining_seconds: status?.calibrationDelayRemainingSeconds ?? 0,
352
+ max_calibration_delays: status?.maxCalibrationDelays ?? 0
353
+ };
354
+ }
355
+ /**
356
+ * The load programs of Backup Switch and smart breaker devices. A site with
357
+ * none returns two empty lists.
358
+ * @param device_ids The devices to read, or every device when omitted.
359
+ */
360
+ async get_controllable_device_programs(device_ids) {
361
+ const din = await this.connect();
362
+ const programs = (await this._send_command_request(din, { teg: { getControllableDeviceProgramsRequest: { deviceIds: device_ids ?? [] } } }, (reply) => reply.teg?.getControllableDeviceProgramsResponse, "get_controllable_device_programs_request")).teg?.getControllableDeviceProgramsResponse;
363
+ return {
364
+ active_programs: (programs?.activePrograms ?? []).map(controllableProgram),
365
+ inactive_programs: (programs?.inactivePrograms ?? []).map(controllableProgram)
366
+ };
367
+ }
368
+ /** The keys paired with the gateway, in the gateway's raw form. */
369
+ async list_authorized_clients() {
370
+ const din = await this.connect();
371
+ const clients = (await this._send_command_request(din, { authorization: { listAuthorizedClientsRequest: {} } }, (reply) => reply.authorization?.listAuthorizedClientsResponse, "list_authorized_clients_request")).authorization?.listAuthorizedClientsResponse;
372
+ return {
373
+ clients: (clients?.clients ?? []).map(authorizedClient),
374
+ enable_line_switch_off: clients?.enableLineSwitchOff ?? false
375
+ };
376
+ }
377
+ /**
378
+ * Patch `config.json`, a read-modify-write. The update carries the hash from
379
+ * the read unchanged: it is an optimistic lock held by the gateway, and the
380
+ * client never computes one.
381
+ * @param updates Dotted paths to values, such as `{"site_info.backup_reserve_percent": 24}`.
382
+ * Writes on one client run one at a time, so two cannot interleave. Another
383
+ * client's write is caught by that lock instead, and refused.
384
+ * @throws RangeError on a number that is not finite, before any request.
385
+ * @throws PowerwallProtocolError on bad JSON, or a reply with no `filestore`.
386
+ */
387
+ async write_config(updates) {
388
+ requireFiniteNumbers(updates, "updates");
389
+ const write = this.#configWrites.then(() => this._write_config(updates));
390
+ this.#configWrites = write.catch(() => void 0);
391
+ return write;
392
+ }
393
+ async _write_config(updates) {
394
+ const din = await this.connect();
395
+ const { blob, hash } = await this._read_config(din);
396
+ const config = decodeConfig(blob);
397
+ for (const [dottedPath, value] of Object.entries(updates)) applyDottedUpdate(config, dottedPath, value);
398
+ if (!(await this._post(_teslemetry_tesla_protocol_energy_device_transport.MessageEnvelope.create({
399
+ ...commandHeader(din),
400
+ filestore: { updateFileRequest: {
401
+ domain: _teslemetry_tesla_protocol_energy_device_filestore_api.FileStoreAPIDomain.FILE_STORE_API_DOMAIN_CONFIG_JSON,
402
+ file: {
403
+ name: CONFIG_FILE,
404
+ blob: Buffer.from(JSON.stringify(config), "utf8")
405
+ },
406
+ hash
407
+ } }
408
+ }), din, "updateFile")).filestore) throw new require_exceptions.PowerwallProtocolError("updateFile response missing filestore payload");
409
+ }
410
+ /**
411
+ * Set the backup reserve on the app's 0-100 scale.
412
+ * @throws RangeError outside 0-100.
413
+ */
414
+ async set_backup_reserve(percent) {
415
+ await this.write_config({ "site_info.backup_reserve_percent": scaled_to_raw_reserve(percent) });
416
+ }
417
+ /**
418
+ * Write the backup reserve on the gateway's raw scale, where app 0% is raw 5%.
419
+ * @throws RangeError outside 0-100.
420
+ */
421
+ async set_backup_reserve_raw(percent) {
422
+ assertPercent(percent, "percent");
423
+ await this.write_config({ "site_info.backup_reserve_percent": percent });
424
+ }
425
+ /**
426
+ * Set the operation mode, as `operation` does through the cloud.
427
+ * @param mode One of `OPERATION_MODES`.
428
+ * @throws RangeError for any other value.
429
+ */
430
+ async set_operation_mode(mode) {
431
+ if (!OPERATION_MODES.includes(mode)) throw new RangeError(`mode must be one of ${OPERATION_MODES.join(", ")}`);
432
+ await this.write_config({ default_real_mode: mode });
433
+ }
434
+ /**
435
+ * Set `strategy.TOU_mode`, which is local-only: the cloud API does not expose it.
436
+ *
437
+ * The gateway does not validate this field, so a typo silently sets an
438
+ * unrecognized mode. Only `economic` is confirmed in use. The tariff itself
439
+ * lives in the cloud and cannot be set here.
440
+ * @throws RangeError on an empty string.
441
+ */
442
+ async set_tou_mode(mode) {
443
+ requireNonEmptyString(mode, "mode");
444
+ await this.write_config({ "strategy.TOU_mode": mode });
445
+ }
446
+ /**
447
+ * Set the export rule, the grid-charging flag, or both in one write. See
448
+ * `set_export_rule` and `set_grid_charging` for one of each.
449
+ * @throws RangeError for an unknown rule.
450
+ * @throws TypeError when given neither.
451
+ */
452
+ async set_grid_import_export(options) {
453
+ const { customer_preferred_export_rule: rule, disallow_charge_from_grid_with_solar_installed: disallow } = options;
454
+ const updates = {};
455
+ if (rule != null) {
456
+ if (!GRID_EXPORT_RULES.includes(rule)) throw new RangeError(`customer_preferred_export_rule must be one of ${GRID_EXPORT_RULES.join(", ")}`);
457
+ updates["site_info.customer_preferred_export_rule"] = rule;
458
+ }
459
+ if (disallow != null) updates["site_info.disallow_charge_from_grid_with_solar_installed"] = Boolean(disallow);
460
+ if (Object.keys(updates).length === 0) throw new TypeError("set_grid_import_export requires customer_preferred_export_rule and/or disallow_charge_from_grid_with_solar_installed");
461
+ await this.write_config(updates);
462
+ }
463
+ /**
464
+ * Set the export rule. `net_meter_mode` is independent and does not move.
465
+ * @param rule `battery_ok` exports solar and battery, `pv_only` solar only, `never` nothing.
466
+ * @throws RangeError for any other value.
467
+ */
468
+ async set_export_rule(rule) {
469
+ await this.set_grid_import_export({ customer_preferred_export_rule: rule });
470
+ }
471
+ /**
472
+ * Allow or disallow charging from the grid. The gateway stores the inverse
473
+ * flag and drops the key rather than store `false`, so treat an absent key
474
+ * as "allowed".
475
+ */
476
+ async set_grid_charging(enabled) {
477
+ await this.set_grid_import_export({ disallow_charge_from_grid_with_solar_installed: !enabled });
478
+ }
479
+ /**
480
+ * Enable on-grid solar curtailment. The gateway drops the key rather than
481
+ * store `false`, so treat an absent key as disabled.
482
+ */
483
+ async set_on_grid_solar_curtailment(enabled) {
484
+ await this.write_config({ "site_info.on_grid_solar_curtailment_enabled": Boolean(enabled) });
485
+ }
486
+ /**
487
+ * Cap grid import, in kilowatts, stored as given.
488
+ * @throws RangeError on a negative value.
489
+ */
490
+ async set_import_limit(kilowatts) {
491
+ requireNonNegative(kilowatts, "kilowatts");
492
+ await this.write_config({ "site_info.max_site_meter_power_ac": kilowatts });
493
+ }
494
+ /**
495
+ * Cap grid export, in kilowatts. Pass a positive magnitude: the gateway
496
+ * represents export as negative site-meter power.
497
+ * @throws RangeError on a negative value.
498
+ */
499
+ async set_export_limit(kilowatts) {
500
+ requireNonNegative(kilowatts, "kilowatts");
501
+ await this.write_config({ "site_info.min_site_meter_power_ac": kilowatts === 0 ? 0 : -kilowatts });
502
+ }
503
+ /**
504
+ * Stop grid export by switching to `backup` mode at a high reserve. This uses
505
+ * a config write, so no contactor cycles, but it takes about 90 seconds to
506
+ * apply. `restore_from_curtailment` puts back what this replaced.
507
+ * @param options.reserve_percent The reserve during curtailment, on the **raw** scale.
508
+ * @throws RangeError outside 0-100.
509
+ */
510
+ async curtail(options = {}) {
511
+ const { reserve_percent = 100 } = options;
512
+ assertPercent(reserve_percent, "reserve_percent");
513
+ if (!this.#curtailmentActive) {
514
+ const config = await this.get_config();
515
+ const reserve = lookup(config, "site_info", "backup_reserve_percent");
516
+ const saved = typeof reserve === "string" ? Number(reserve || NaN) : reserve;
517
+ this.#savedRealMode = String(config.default_real_mode || "self_consumption");
518
+ this.#savedReservePercent = typeof saved === "number" && Number.isFinite(saved) ? saved : OFFSET;
519
+ }
520
+ await this.write_config({
521
+ default_real_mode: "backup",
522
+ "site_info.backup_reserve_percent": reserve_percent
523
+ });
524
+ this.#curtailmentActive = true;
525
+ }
526
+ /** Write back what `curtail` replaced, or `self_consumption` at a raw 5 if nothing was saved. */
527
+ async restore_from_curtailment() {
528
+ await this.write_config({
529
+ default_real_mode: this.#savedRealMode ?? "self_consumption",
530
+ "site_info.backup_reserve_percent": this.#savedReservePercent ?? OFFSET
531
+ });
532
+ this.#curtailmentActive = false;
533
+ }
534
+ /**
535
+ * Schedule a manual "max backup" event that starts now, at maximum priority.
536
+ * The gateway requires any earlier event to be cancelled first, even an
537
+ * expired one, so this sends that cancel before scheduling.
538
+ * @param duration_seconds At least 60.
539
+ * @throws RangeError on a shorter or fractional duration.
540
+ */
541
+ async schedule_max_backup(duration_seconds = DEFAULT_BACKUP_DURATION_SECONDS) {
542
+ requireIntegerInRange(duration_seconds, "duration_seconds", MIN_BACKUP_DURATION_SECONDS, MAX_UINT32);
543
+ const din = await this.connect();
544
+ await this._cancel_manual_backup(din);
545
+ await this._send_command_request(din, { teg: { scheduleManualBackupEventRequest: { schedulingInfo: {
546
+ startTime: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3),
547
+ durationSeconds: duration_seconds,
548
+ priority: MAX_PRIORITY
549
+ } } } }, (reply) => reply.teg?.scheduleManualBackupEventResponse, "schedule_manual_backup_event_request");
550
+ }
551
+ /** Cancel the active manual backup event, if any. */
552
+ async cancel_max_backup() {
553
+ await this._cancel_manual_backup(await this.connect());
554
+ }
555
+ /**
556
+ * Request a timed battery power setpoint.
557
+ *
558
+ * The gateway holds it for the duration and then returns to its mode by
559
+ * itself. A new call replaces the active setpoint, power and timer both. The
560
+ * reply only acknowledges the request, so read `get_meters_aggregates` to see
561
+ * what the battery does. Keep durations short and renew them: a third party
562
+ * reports a dispatch discharging below reserve and exporting regardless of the
563
+ * export setting. Verified on Powerwall 3 firmware 26.26.4.
564
+ * @param power_watts Positive discharges, negative charges.
565
+ * @param duration_seconds Whole seconds from 1 to 86400.
566
+ * @throws PowerwallCommandError with code 11 above the site limit. The gateway refuses, and does not clamp.
567
+ * @throws PowerwallNotSupportedError on firmware without this command.
568
+ */
569
+ async dispatch_battery_power(power_watts, duration_seconds) {
570
+ require_energyspecific.validate_dispatch_battery_power(power_watts, duration_seconds);
571
+ const din = await this.connect();
572
+ await this._send_command_request(din, { teg: { dispatchBatteryPowerRequest: { realPower: {
573
+ powerWatts: power_watts,
574
+ durationSeconds: duration_seconds
575
+ } } } }, (reply) => reply.teg?.dispatchBatteryPowerResponse, "dispatch_battery_power_request");
576
+ }
577
+ /** Release a `dispatch_battery_power` setpoint. This does not cancel a manual backup event. */
578
+ async resume_battery_operation() {
579
+ const din = await this.connect();
580
+ await this._send_command_request(din, { teg: { dispatchBatteryPowerRequest: { resumeDeviceMode: {} } } }, (reply) => reply.teg?.dispatchBatteryPowerResponse, "dispatch_battery_power_request");
581
+ }
582
+ /**
583
+ * Postpone the next battery calibration. Powerwall 3 firmware 26.26.4
584
+ * acknowledges this, but its effect is unverified: that firmware cannot report
585
+ * the delay status.
586
+ * @param delay_hours An integer from 1 to `2^32 - 1`.
587
+ */
588
+ async delay_battery_calibration(delay_hours) {
589
+ requireIntegerInRange(delay_hours, "delay_hours", 1, MAX_UINT32);
590
+ const din = await this.connect();
591
+ await this._send_command_request(din, { teg: { delayBatteryCalibrationRequest: { delayHours: delay_hours } } }, (reply) => reply.teg?.delayBatteryCalibrationResponse, "delay_battery_calibration_request");
592
+ }
593
+ /**
594
+ * Point the gateway at an OCPP central system.
595
+ * @throws PowerwallNotSupportedError on firmware without this command. Powerwall 3 firmware 26.26.4 is one.
596
+ */
597
+ async configure_ocpp(csms_base_url) {
598
+ requireNonEmptyString(csms_base_url, "csms_base_url");
599
+ const din = await this.connect();
600
+ await this._send_command_request(din, { teg: { configureOcppRequest: { csmsBaseUrl: csms_base_url } } }, (reply) => reply.teg?.configureOcppResponse, "configure_ocpp_request");
601
+ }
602
+ /**
603
+ * Revoke a paired key. This needs no presence proof, and any paired key can
604
+ * remove any other. Removing the signing key locks this client out, and
605
+ * pairing it again needs the usual registration and breaker toggle. A success
606
+ * carries no fields, so call `list_authorized_clients` to confirm.
607
+ * @param public_key Raw DER bytes, or base64 as `list_authorized_clients` reports it.
608
+ * @throws RangeError on an empty key.
609
+ */
610
+ async remove_authorized_client(public_key) {
611
+ const publicKey = typeof public_key === "string" ? Buffer.from(public_key, "base64") : public_key;
612
+ if (!(publicKey instanceof Uint8Array)) throw new TypeError("public_key must be bytes or a base64 string");
613
+ if (publicKey.length === 0) throw new RangeError("public_key must not be empty");
614
+ const din = await this.connect();
615
+ await this._send_command_request(din, { authorization: { removeAuthorizedClientRequest: { publicKey } } }, (reply) => reply.authorization?.removeAuthorizedClientResponse, "remove_authorized_client_request", "empty");
616
+ }
617
+ /**
618
+ * Ask the gateway to go off grid or reconnect.
619
+ *
620
+ * A success-shaped reply does not prove the contactor moved: PowerSync reports
621
+ * firmware that acknowledges without acting. Read `islanding.contactorClosed`
622
+ * from `get_status` afterwards before relying on it.
623
+ * @param options.off_grid Mode 6 (off grid) when true, mode 1 (on grid) when false.
624
+ * @param options.force Required by some Powerwalls.
625
+ * @param options.mode_override Send this mode instead: 1 or 6.
626
+ * @throws TypeError on any other mode, or an `off_grid` that is not a boolean.
627
+ * @throws PowerwallCommandError on a `REJECTED_*` result, with `code` the enum value and `status_message` its name, or on `common.error_response`.
628
+ */
629
+ async set_island_mode(options) {
630
+ const { off_grid, force = false, mode_override } = options;
631
+ if (typeof off_grid !== "boolean") throw new TypeError("off_grid must be a boolean");
632
+ const mode = mode_override ?? (off_grid ? _teslemetry_tesla_protocol_energy_device_teg_api.TEGIslandMode.TEG_ISLAND_MODE_INTENTIONAL_WITH_RECONNECT_FAILSAFE : _teslemetry_tesla_protocol_energy_device_teg_api.TEGIslandMode.TEG_ISLAND_MODE_AUTO_ACTIVE);
633
+ require_energyisland.validate_island_mode(mode, force);
634
+ const din = await this.connect();
635
+ const result = (await this._send_command_request(din, { teg: { setIslandModeRequest: {
636
+ mode,
637
+ force
638
+ } } }, (reply) => reply.teg?.setIslandModeResponse, "set_island_mode_request", "unverified")).teg?.setIslandModeResponse?.result;
639
+ this.#logger?.debug(`set_island_mode: mode=${mode} force=${force} result=${result ?? "absent"}`);
640
+ const rejection = require_energyisland.island_mode_rejection(result);
641
+ if (rejection) throw new require_exceptions.PowerwallCommandError("set_island_mode_request", rejection.code, rejection.name);
642
+ }
643
+ /**
644
+ * Disconnect from the grid. See `set_island_mode`, including its warning: read
645
+ * `islanding.contactorClosed` afterwards.
646
+ * @param options.force Defaults to true.
647
+ */
648
+ async go_off_grid(options = {}) {
649
+ const { force = true, mode_override } = options;
650
+ await this.set_island_mode({
651
+ off_grid: true,
652
+ force,
653
+ mode_override
654
+ });
655
+ }
656
+ /** Reconnect to the grid. See `set_island_mode`, including its warning: read `islanding.contactorClosed` afterwards. */
657
+ async reconnect_grid() {
658
+ await this.set_island_mode({
659
+ off_grid: false,
660
+ force: false
661
+ });
662
+ }
663
+ /**
664
+ * The explicit black-start command, which drives the full islanding
665
+ * transition: grid-frequency ramp-down, contactor open, inverter restart.
666
+ * `set_island_mode` only updates the desired mode. No hardware result is
667
+ * recorded for this command, so check the contactor afterwards.
668
+ * @throws PowerwallCommandError on `common.error_response`.
669
+ */
670
+ async trigger_islanding() {
671
+ const din = await this.connect();
672
+ await this._send_command_request(din, { teg: { triggerIslandingBlackStartRequest: {} } }, (reply) => reply.teg?.triggerIslandingBlackStartResponse, "trigger_islanding_black_start_request", "unverified");
673
+ }
674
+ async _cancel_manual_backup(din) {
675
+ await this._send_command_request(din, { teg: { cancelManualBackupEventRequest: {} } }, (reply) => reply.teg?.cancelManualBackupEventResponse, "cancel_manual_backup_event_request", "empty", STATUS_NOT_FOUND);
676
+ }
677
+ async _query_graphql(query, code, variables) {
678
+ const din = await this.connect();
679
+ const reply = (await this._post(_teslemetry_tesla_protocol_energy_device_transport.MessageEnvelope.create({
680
+ ...localHeader(din),
681
+ graphql: { queryRequest: {
682
+ format: _teslemetry_tesla_protocol_energy_device_graphql_api.GraphQLQueryFormat.GRAPH_QL_QUERY_FORMAT_SIGNED_SHA256_ECDSA_ASN1,
683
+ query: _teslemetry_tesla_protocol_energy_device_graphql_api.SignedGraphQLQuery.encode({
684
+ version: 1,
685
+ query: Buffer.from(query, "utf8")
686
+ }).finish(),
687
+ signature: code,
688
+ variablesJson: variables
689
+ } }
690
+ }), din, "query")).graphql?.queryResponse;
691
+ if (!reply) throw new require_exceptions.PowerwallProtocolError("v1r query response missing payload");
692
+ if (!reply.data) throw new require_exceptions.PowerwallProtocolError("v1r query response payload is empty");
693
+ return reply.data;
694
+ }
695
+ async _read_config(din) {
696
+ const response = await this._post(_teslemetry_tesla_protocol_energy_device_transport.MessageEnvelope.create({
697
+ ...commandHeader(din),
698
+ filestore: { readFileRequest: {
699
+ domain: _teslemetry_tesla_protocol_energy_device_filestore_api.FileStoreAPIDomain.FILE_STORE_API_DOMAIN_CONFIG_JSON,
700
+ name: CONFIG_FILE
701
+ } }
702
+ }), din, "FileStore");
703
+ if (!response.filestore) throw new require_exceptions.PowerwallProtocolError("FileStore response missing filestore payload");
704
+ const reply = response.filestore.readFileResponse;
705
+ return {
706
+ blob: reply?.file?.blob ?? /* @__PURE__ */ new Uint8Array(),
707
+ hash: reply?.hash ?? /* @__PURE__ */ new Uint8Array()
708
+ };
709
+ }
710
+ /** Sign, send once, and decode the reply envelope. */
711
+ async _post(envelope, din, what) {
712
+ return decodeEnvelope(await this.#transport.post_v1r(_teslemetry_tesla_protocol_energy_device_transport.MessageEnvelope.encode(envelope).finish(), din), what);
713
+ }
714
+ /**
715
+ * The one path for Command-header messages.
716
+ *
717
+ * Python's tolerated calls swallow every error, so a BUSY fault while revoking
718
+ * a compromised key reports success with the key still paired. Here `empty`
719
+ * accepts only an empty reply, and `ignored_status` the one refusal that means
720
+ * there was nothing to do.
721
+ * @param din The gateway DIN.
722
+ * @param payload The message's category and request.
723
+ * @param answered Reads the expected response field from a reply.
724
+ * @param request The request's proto field name, for errors.
725
+ * @param missing What to do when the reply lacks the response.
726
+ * @param ignored_status A `common.error_response` code that is not a failure.
727
+ */
728
+ async _send_command_request(din, payload, answered, request, missing = "raise", ignored_status) {
729
+ let inner;
730
+ try {
731
+ inner = await this.#transport.post_v1r(_teslemetry_tesla_protocol_energy_device_transport.MessageEnvelope.encode(_teslemetry_tesla_protocol_energy_device_transport.MessageEnvelope.create({
732
+ ...commandHeader(din),
733
+ ...payload
734
+ })).finish(), din);
735
+ } catch (error) {
736
+ if (missing === "empty" && error instanceof require_transport.PowerwallEmptyReplyError) return _teslemetry_tesla_protocol_energy_device_transport.MessageEnvelope.create();
737
+ throw error;
738
+ }
739
+ const response = decodeEnvelope(inner, request);
740
+ if (answered(response) !== void 0) return response;
741
+ const status = response.common?.errorResponse?.status;
742
+ if (status && status.code === ignored_status) return response;
743
+ if (status) throw new (status.code === STATUS_UNIMPLEMENTED ? require_exceptions.PowerwallNotSupportedError : require_exceptions.PowerwallCommandError)(request, status.code, status.message);
744
+ if (missing === "unverified" || missing === "empty" && isEmptyReply(response)) {
745
+ this.#logger?.debug(`${request}: reply has no response (${inner.length} bytes)`);
746
+ return response;
747
+ }
748
+ throw new require_exceptions.PowerwallProtocolError(`${request} reply missing its response`);
749
+ }
750
+ };
751
+ /**
752
+ * Convert a reserve percentage from the app's scale to the gateway's raw value.
753
+ * @throws RangeError outside 0-100.
754
+ */
755
+ function scaled_to_raw_reserve(percent) {
756
+ return scaledToRaw(percent);
757
+ }
758
+ /** Convert a raw gateway reserve value to the app's scale. */
759
+ function raw_to_scaled_reserve(percent) {
760
+ return rawToScaled(percent);
761
+ }
762
+ /**
763
+ * Convert a state of charge from the app's scale to the raw value.
764
+ * @throws RangeError outside 0-100.
765
+ */
766
+ function scaled_to_raw_soc(percent) {
767
+ return scaledToRaw(percent);
768
+ }
769
+ /** Convert a raw state of charge to the value the Tesla app and Fleet API show. */
770
+ function raw_to_scaled_soc(percent) {
771
+ return rawToScaled(percent);
772
+ }
773
+ function scaledToRaw(percent) {
774
+ assertPercent(percent, "percent");
775
+ return round4(percent * SCALE + OFFSET);
776
+ }
777
+ function rawToScaled(percent) {
778
+ return round4((percent - OFFSET) / SCALE);
779
+ }
780
+ function assertPercent(value, name) {
781
+ if (typeof value !== "number") throw new TypeError(`${name} must be a number`);
782
+ if (!(value >= 0 && value <= 100)) throw new RangeError(`${name} must be between 0 and 100`);
783
+ }
784
+ /** The raw physical state of charge from a status payload, or `null` when either figure is missing or zero. */
785
+ function battery_level_raw(status) {
786
+ const remaining = Number(lookup(status, "control", "systemStatus", "nominalEnergyRemainingWh") ?? NaN);
787
+ const full = Number(lookup(status, "control", "systemStatus", "nominalFullPackEnergyWh") ?? NaN);
788
+ return remaining && full ? remaining / full * 100 : null;
789
+ }
790
+ /** The state of charge the Tesla app and Fleet API show, from a status payload, or `null`. */
791
+ function battery_level(status) {
792
+ const raw = battery_level_raw(status);
793
+ return raw === null ? null : raw_to_scaled_soc(raw);
794
+ }
795
+ /** Real power in watts by upper-case meter location. Malformed aggregates give `{}`. */
796
+ function current_power(status) {
797
+ const meters = lookup(status, "control", "meterAggregates");
798
+ if (!Array.isArray(meters)) return {};
799
+ return Object.fromEntries(meters.filter((meter) => isRecord(meter) && meter.location != null).map((meter) => [String(meter.location).toUpperCase(), meter.realPowerW]));
800
+ }
801
+ /** Backup runtime in hours at the current load, or `null` when unknown. */
802
+ function backup_time_remaining(status) {
803
+ const remaining = Number(lookup(status, "control", "systemStatus", "nominalEnergyRemainingWh") ?? NaN);
804
+ const load = current_power(status).LOAD;
805
+ return remaining && typeof load === "number" && load > 0 ? remaining / load : null;
806
+ }
807
+ exports.DEFAULT_GATEWAY_HOST = DEFAULT_GATEWAY_HOST;
808
+ exports.GRID_EXPORT_RULES = GRID_EXPORT_RULES;
809
+ exports.OPERATION_MODES = OPERATION_MODES;
810
+ exports.PowerwallClient = PowerwallClient;
811
+ exports.backup_time_remaining = backup_time_remaining;
812
+ exports.battery_level = battery_level;
813
+ exports.battery_level_raw = battery_level_raw;
814
+ exports.current_power = current_power;
815
+ exports.raw_to_scaled_reserve = raw_to_scaled_reserve;
816
+ exports.raw_to_scaled_soc = raw_to_scaled_soc;
817
+ exports.scaled_to_raw_reserve = scaled_to_raw_reserve;
818
+ exports.scaled_to_raw_soc = scaled_to_raw_soc;