node-chargepoint 0.0.1
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/LICENSE +21 -0
- package/README.md +460 -0
- package/dist/cli.cjs +814 -0
- package/dist/index.cjs +712 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +438 -0
- package/dist/index.d.ts +438 -0
- package/dist/index.js +700 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
|
|
3
|
+
// src/constants.ts
|
|
4
|
+
var DISCOVERY_API = "https://discovery.chargepoint.com/discovery/v3/globalconfig";
|
|
5
|
+
var VERSION = "0.0.1-alpha.0";
|
|
6
|
+
var USER_AGENT = `node-chargepoint/${VERSION}`;
|
|
7
|
+
|
|
8
|
+
// src/exceptions.ts
|
|
9
|
+
var APIError = class extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "APIError";
|
|
13
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var CommunicationError = class extends APIError {
|
|
17
|
+
constructor(statusCode, message, body) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
this.body = body;
|
|
21
|
+
this.name = "CommunicationError";
|
|
22
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
23
|
+
}
|
|
24
|
+
statusCode;
|
|
25
|
+
body;
|
|
26
|
+
};
|
|
27
|
+
var LoginError = class extends CommunicationError {
|
|
28
|
+
constructor(statusCode, message, body) {
|
|
29
|
+
super(statusCode, message, body);
|
|
30
|
+
this.name = "LoginError";
|
|
31
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var InvalidSession = class extends CommunicationError {
|
|
35
|
+
constructor(statusCode = 401, message = "ChargePoint session expired. Please log in again.", body) {
|
|
36
|
+
super(statusCode, message, body);
|
|
37
|
+
this.name = "InvalidSession";
|
|
38
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var DatadomeCaptcha = class extends APIError {
|
|
42
|
+
constructor(captchaUrl, message = "Datadome captcha protection triggered.") {
|
|
43
|
+
super(message);
|
|
44
|
+
this.captchaUrl = captchaUrl;
|
|
45
|
+
this.name = "DatadomeCaptcha";
|
|
46
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
47
|
+
}
|
|
48
|
+
captchaUrl;
|
|
49
|
+
};
|
|
50
|
+
async function postJson(url, body) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const bodyBuf = Buffer.from(body, "utf-8");
|
|
53
|
+
const req = https.request(
|
|
54
|
+
url,
|
|
55
|
+
{
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"Content-Length": bodyBuf.byteLength,
|
|
60
|
+
"User-Agent": USER_AGENT
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
(res) => {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
66
|
+
res.on("end", () => {
|
|
67
|
+
const statusCode = res.statusCode ?? 0;
|
|
68
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
69
|
+
reject(new CommunicationError(statusCode, "Failed to fetch global configuration."));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
resolve({ status: statusCode, data: JSON.parse(Buffer.concat(chunks).toString("utf-8")) });
|
|
74
|
+
} catch {
|
|
75
|
+
reject(new CommunicationError(statusCode, "Failed to parse global configuration response."));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
req.on("error", (err) => {
|
|
81
|
+
reject(new CommunicationError(0, `Failed to reach ChargePoint discovery API: ${err.message}`));
|
|
82
|
+
});
|
|
83
|
+
req.write(bodyBuf);
|
|
84
|
+
req.end();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function endpointStr(v) {
|
|
88
|
+
const raw = v !== null && typeof v === "object" && "value" in v ? String(v.value ?? "") : typeof v === "string" ? v : "";
|
|
89
|
+
return raw.replace(/\/+$/, "");
|
|
90
|
+
}
|
|
91
|
+
function parseEndpoints(raw) {
|
|
92
|
+
const get = (camel, snake) => endpointStr(raw[camel] ?? raw[snake] ?? "");
|
|
93
|
+
return {
|
|
94
|
+
accountsEndpoint: get("accountsEndpoint", "accounts_endpoint"),
|
|
95
|
+
internalApiGatewayEndpoint: get("internalApiGatewayEndpoint", "internal_api_gateway_endpoint"),
|
|
96
|
+
mapcacheEndpoint: get("mapcacheEndpoint", "mapcache_endpoint"),
|
|
97
|
+
pandaWebsocketEndpoint: get("pandaWebsocketEndpoint", "panda_websocket_endpoint"),
|
|
98
|
+
paymentJavaEndpoint: get("paymentJavaEndpoint", "payment_java_endpoint"),
|
|
99
|
+
paymentPhpEndpoint: get("paymentPhpEndpoint", "payment_php_endpoint"),
|
|
100
|
+
portalDomainEndpoint: get("portalDomainEndpoint", "portal_domain_endpoint"),
|
|
101
|
+
portalSubdomain: typeof raw.portalSubdomain === "string" ? raw.portalSubdomain : typeof raw.portal_subdomain === "string" ? raw.portal_subdomain : "",
|
|
102
|
+
ssoEndpoint: get("ssoEndpoint", "sso_endpoint"),
|
|
103
|
+
webservicesEndpoint: get("webservicesEndpoint", "webservices_endpoint"),
|
|
104
|
+
websocketEndpoint: get("websocketEndpoint", "websocket_endpoint"),
|
|
105
|
+
hcpoHcmEndpoint: get("hcpoHcmEndpoint", "hcpo_hcm_endpoint")
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function fetchGlobalConfig(region = "NA") {
|
|
109
|
+
const { data } = await postJson(DISCOVERY_API, JSON.stringify({ regionCode: region }));
|
|
110
|
+
const raw = typeof data.globalConfiguration === "object" && data.globalConfiguration !== null ? data.globalConfiguration : data;
|
|
111
|
+
const endpointsRaw = raw.endPoints ?? raw.endpoints ?? {};
|
|
112
|
+
return {
|
|
113
|
+
region: typeof raw.region === "string" ? raw.region : region,
|
|
114
|
+
defaultCountry: raw.defaultCountry ?? {},
|
|
115
|
+
supportedCountries: Array.isArray(raw.supportedCountries) ? raw.supportedCountries : [],
|
|
116
|
+
defaultCurrency: raw.currency ?? raw.defaultCurrency ?? {},
|
|
117
|
+
supportedCurrencies: Array.isArray(raw.supportedCurrencies) ? raw.supportedCurrencies : [],
|
|
118
|
+
endpoints: parseEndpoints(endpointsRaw)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/session.ts
|
|
123
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
124
|
+
async function sendCommand(client, action, deviceId, portNumber = 1, sessionId = 0) {
|
|
125
|
+
const actionPath = action === "start" ? "startsession" : "stopSession";
|
|
126
|
+
const body = { deviceId };
|
|
127
|
+
if (action === "stop") {
|
|
128
|
+
body.portNumber = portNumber;
|
|
129
|
+
body.sessionId = sessionId;
|
|
130
|
+
}
|
|
131
|
+
const url = `${client.globalConfig.endpoints.accountsEndpoint}/v1/driver/station/${actionPath}`;
|
|
132
|
+
const response = await client._request("POST", url, {
|
|
133
|
+
body: JSON.stringify(body),
|
|
134
|
+
headers: { "Content-Type": "application/json" }
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const text = await response.text();
|
|
138
|
+
throw new CommunicationError(
|
|
139
|
+
response.status,
|
|
140
|
+
`Failed to ${action} ChargePoint session: ${text}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const actionStatus = await response.json();
|
|
144
|
+
const ackId = actionStatus.ackId;
|
|
145
|
+
const ackUrl = `${client.globalConfig.endpoints.accountsEndpoint}/v1/driver/station/session/ack`;
|
|
146
|
+
let lastStatus = 0;
|
|
147
|
+
let errorMessage = `Session failed to ${action}.`;
|
|
148
|
+
let errorBody;
|
|
149
|
+
for (let attempt = 1; attempt <= 20; attempt++) {
|
|
150
|
+
const ackResponse = await client._request("POST", ackUrl, {
|
|
151
|
+
body: JSON.stringify({ ackId, action: `${action}_session` }),
|
|
152
|
+
headers: { "Content-Type": "application/json" }
|
|
153
|
+
});
|
|
154
|
+
lastStatus = ackResponse.status;
|
|
155
|
+
if (ackResponse.status === 200) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
errorBody = await ackResponse.json();
|
|
160
|
+
const msg = errorBody.errorMessage;
|
|
161
|
+
if (typeof msg === "string") errorMessage = msg;
|
|
162
|
+
} catch {
|
|
163
|
+
errorBody = void 0;
|
|
164
|
+
}
|
|
165
|
+
if (attempt < 20) {
|
|
166
|
+
await sleep(3e3);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw new CommunicationError(lastStatus, errorMessage, errorBody);
|
|
170
|
+
}
|
|
171
|
+
function parseMsTimestamp(v) {
|
|
172
|
+
if (typeof v === "number") return new Date(v);
|
|
173
|
+
if (typeof v === "string") return new Date(Number(v));
|
|
174
|
+
return /* @__PURE__ */ new Date(0);
|
|
175
|
+
}
|
|
176
|
+
function parseSessionUpdates(raw) {
|
|
177
|
+
if (!Array.isArray(raw)) return [];
|
|
178
|
+
return raw.map((u) => {
|
|
179
|
+
const item = u;
|
|
180
|
+
return {
|
|
181
|
+
energyKwh: typeof item.energy_kwh === "number" ? item.energy_kwh : 0,
|
|
182
|
+
powerKw: typeof item.power_kw === "number" ? item.power_kw : 0,
|
|
183
|
+
timestamp: parseMsTimestamp(item.timestamp)
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
var ChargingSession = class _ChargingSession {
|
|
188
|
+
sessionId;
|
|
189
|
+
deviceId = 0;
|
|
190
|
+
deviceName = "";
|
|
191
|
+
chargingState = "";
|
|
192
|
+
chargingTime = 0;
|
|
193
|
+
energyKwh = 0;
|
|
194
|
+
milesAdded = 0;
|
|
195
|
+
milesAddedPerHour = 0;
|
|
196
|
+
outletNumber = 0;
|
|
197
|
+
portLevel = 0;
|
|
198
|
+
powerKw = 0;
|
|
199
|
+
purpose = "";
|
|
200
|
+
currencyIsoCode = "";
|
|
201
|
+
paymentCompleted = false;
|
|
202
|
+
paymentType = "";
|
|
203
|
+
pricingSpecId = 0;
|
|
204
|
+
totalAmount = 0;
|
|
205
|
+
apiFlag = false;
|
|
206
|
+
enableStopCharging = false;
|
|
207
|
+
hasChargingReceipt = false;
|
|
208
|
+
hasUtilityInfo = false;
|
|
209
|
+
isHomeCharger = false;
|
|
210
|
+
isPurposeFinalized = false;
|
|
211
|
+
stopChargeSupported = false;
|
|
212
|
+
companyId = 0;
|
|
213
|
+
companyName = "";
|
|
214
|
+
latitude = 0;
|
|
215
|
+
longitude = 0;
|
|
216
|
+
address = "";
|
|
217
|
+
city = "";
|
|
218
|
+
stateName = "";
|
|
219
|
+
country = "";
|
|
220
|
+
zipcode = "";
|
|
221
|
+
updatePeriod = 0;
|
|
222
|
+
startTime = null;
|
|
223
|
+
lastUpdateDataTimestamp = null;
|
|
224
|
+
updateData = null;
|
|
225
|
+
utility = null;
|
|
226
|
+
vehicleInfo = null;
|
|
227
|
+
_client = null;
|
|
228
|
+
constructor(sessionId) {
|
|
229
|
+
this.sessionId = sessionId;
|
|
230
|
+
}
|
|
231
|
+
/** @internal */
|
|
232
|
+
_setClient(client) {
|
|
233
|
+
this._client = client;
|
|
234
|
+
}
|
|
235
|
+
/** @internal Apply raw session data from the driver-bff API (snake_case keys). */
|
|
236
|
+
_apply(data) {
|
|
237
|
+
if (data.device_id !== void 0) this.deviceId = data.device_id;
|
|
238
|
+
if (data.device_name !== void 0) this.deviceName = data.device_name;
|
|
239
|
+
if (data.current_charging !== void 0) this.chargingState = data.current_charging;
|
|
240
|
+
if (data.charging_time !== void 0) this.chargingTime = data.charging_time;
|
|
241
|
+
if (data.energy_kwh !== void 0) this.energyKwh = data.energy_kwh;
|
|
242
|
+
if (data.miles_added !== void 0) this.milesAdded = data.miles_added;
|
|
243
|
+
if (data.miles_added_per_hour !== void 0) this.milesAddedPerHour = data.miles_added_per_hour;
|
|
244
|
+
if (data.outlet_number !== void 0) this.outletNumber = data.outlet_number;
|
|
245
|
+
if (data.port_level !== void 0) this.portLevel = data.port_level;
|
|
246
|
+
if (data.power_kw !== void 0) this.powerKw = data.power_kw;
|
|
247
|
+
if (data.purpose !== void 0) this.purpose = data.purpose;
|
|
248
|
+
if (data.currency_iso_code !== void 0) this.currencyIsoCode = String(data.currency_iso_code);
|
|
249
|
+
if (data.payment_completed !== void 0) this.paymentCompleted = data.payment_completed;
|
|
250
|
+
if (data.payment_type !== void 0) this.paymentType = data.payment_type;
|
|
251
|
+
if (data.pricing_spec_id !== void 0) this.pricingSpecId = data.pricing_spec_id;
|
|
252
|
+
if (data.total_amount !== void 0) this.totalAmount = data.total_amount;
|
|
253
|
+
if (data.api_flag !== void 0) this.apiFlag = data.api_flag;
|
|
254
|
+
if (data.enable_stop_charging !== void 0) this.enableStopCharging = data.enable_stop_charging;
|
|
255
|
+
if (data.has_charging_receipt !== void 0) this.hasChargingReceipt = data.has_charging_receipt;
|
|
256
|
+
if (data.has_utility_info !== void 0) this.hasUtilityInfo = data.has_utility_info;
|
|
257
|
+
if (data.is_home_charger !== void 0) this.isHomeCharger = data.is_home_charger;
|
|
258
|
+
if (data.is_purpose_finalized !== void 0) this.isPurposeFinalized = data.is_purpose_finalized;
|
|
259
|
+
if (data.stop_charge_supported !== void 0) this.stopChargeSupported = data.stop_charge_supported;
|
|
260
|
+
if (data.company_id !== void 0) this.companyId = data.company_id;
|
|
261
|
+
if (data.company_name !== void 0) this.companyName = data.company_name;
|
|
262
|
+
if (data.lat !== void 0) this.latitude = data.lat;
|
|
263
|
+
if (data.lon !== void 0) this.longitude = data.lon;
|
|
264
|
+
if (data.address1 !== void 0) this.address = data.address1;
|
|
265
|
+
if (data.city !== void 0) this.city = data.city;
|
|
266
|
+
if (data.state_name !== void 0) this.stateName = data.state_name;
|
|
267
|
+
if (data.country !== void 0) this.country = data.country;
|
|
268
|
+
if (data.zipcode !== void 0) this.zipcode = data.zipcode;
|
|
269
|
+
if (data.update_period !== void 0) this.updatePeriod = data.update_period;
|
|
270
|
+
if (data.start_time !== void 0) this.startTime = parseMsTimestamp(data.start_time);
|
|
271
|
+
if (data.last_update_data_timestamp !== void 0) {
|
|
272
|
+
this.lastUpdateDataTimestamp = parseMsTimestamp(data.last_update_data_timestamp);
|
|
273
|
+
}
|
|
274
|
+
if (data.update_data !== void 0) this.updateData = parseSessionUpdates(data.update_data);
|
|
275
|
+
if (data.utility !== void 0) this.utility = data.utility ?? null;
|
|
276
|
+
if (data.vehicle_info !== void 0) this.vehicleInfo = data.vehicle_info ?? null;
|
|
277
|
+
}
|
|
278
|
+
async refresh() {
|
|
279
|
+
if (!this._client) throw new Error("ChargingSession client not set.");
|
|
280
|
+
const url = `${this._client.globalConfig.endpoints.internalApiGatewayEndpoint}/driver-bff/v1/sessions/${this.sessionId}`;
|
|
281
|
+
const response = await this._client._request("POST", url, {
|
|
282
|
+
body: JSON.stringify({
|
|
283
|
+
charging_status: { session_id: this.sessionId, mfhs: [] }
|
|
284
|
+
}),
|
|
285
|
+
headers: { "Content-Type": "application/json" }
|
|
286
|
+
});
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
throw new CommunicationError(response.status, "Failed to get charging session data.");
|
|
289
|
+
}
|
|
290
|
+
const json = await response.json();
|
|
291
|
+
const status = json.charging_status;
|
|
292
|
+
if (!status || "error_message" in status || "error" in status) {
|
|
293
|
+
throw new CommunicationError(response.status, "Failed to get charging session data.");
|
|
294
|
+
}
|
|
295
|
+
this._apply(status);
|
|
296
|
+
}
|
|
297
|
+
async stop() {
|
|
298
|
+
if (!this._client) throw new Error("ChargingSession client not set.");
|
|
299
|
+
await sendCommand(
|
|
300
|
+
this._client,
|
|
301
|
+
"stop",
|
|
302
|
+
this.deviceId,
|
|
303
|
+
this.outletNumber,
|
|
304
|
+
this.sessionId
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
static async start(deviceId, client) {
|
|
308
|
+
await sendCommand(client, "start", deviceId);
|
|
309
|
+
const status = await client.getUserChargingStatus();
|
|
310
|
+
if (!status) {
|
|
311
|
+
throw new APIError("No active charging session found after start command.");
|
|
312
|
+
}
|
|
313
|
+
const session = new _ChargingSession(status.sessionId);
|
|
314
|
+
session._setClient(client);
|
|
315
|
+
await session.refresh();
|
|
316
|
+
return session;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// src/client.ts
|
|
321
|
+
function parseMsTimestamp2(v) {
|
|
322
|
+
if (typeof v === "number") return new Date(v);
|
|
323
|
+
if (typeof v === "string") return new Date(Number(v));
|
|
324
|
+
return /* @__PURE__ */ new Date(0);
|
|
325
|
+
}
|
|
326
|
+
var ChargePoint = class _ChargePoint {
|
|
327
|
+
globalConfig;
|
|
328
|
+
_username;
|
|
329
|
+
_coulombToken = null;
|
|
330
|
+
_region;
|
|
331
|
+
_userId = null;
|
|
332
|
+
/** The current session token. Save this after login to avoid re-authenticating. */
|
|
333
|
+
get coulombToken() {
|
|
334
|
+
return this._coulombToken;
|
|
335
|
+
}
|
|
336
|
+
constructor(username, globalConfig, region) {
|
|
337
|
+
this._username = username;
|
|
338
|
+
this.globalConfig = globalConfig;
|
|
339
|
+
this._region = region;
|
|
340
|
+
}
|
|
341
|
+
static async create(username, options = {}) {
|
|
342
|
+
const region = options.region ?? "NA";
|
|
343
|
+
const config = await fetchGlobalConfig(region);
|
|
344
|
+
const client = new _ChargePoint(username, config, region);
|
|
345
|
+
if (options.coulombToken) {
|
|
346
|
+
client._setToken(options.coulombToken, region);
|
|
347
|
+
}
|
|
348
|
+
return client;
|
|
349
|
+
}
|
|
350
|
+
_setToken(token, region) {
|
|
351
|
+
this._coulombToken = token;
|
|
352
|
+
this._region = region;
|
|
353
|
+
}
|
|
354
|
+
/** @internal Used by session.ts and tests. */
|
|
355
|
+
async _request(method, url, init = {}) {
|
|
356
|
+
const headers = new Headers(init.headers);
|
|
357
|
+
headers.set("user-agent", USER_AGENT);
|
|
358
|
+
if (!headers.has("content-type") && method !== "GET") {
|
|
359
|
+
headers.set("content-type", "application/json");
|
|
360
|
+
}
|
|
361
|
+
if (this._coulombToken) {
|
|
362
|
+
headers.set("cookie", `coulomb_sess=${this._coulombToken}`);
|
|
363
|
+
headers.set("cp-session-type", "CP_SESSION_TOKEN");
|
|
364
|
+
headers.set("cp-session-token", this._coulombToken);
|
|
365
|
+
headers.set("cp-region", this._region);
|
|
366
|
+
}
|
|
367
|
+
const response = await fetch(url, { ...init, method, headers });
|
|
368
|
+
const setCookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [response.headers.get("set-cookie") ?? ""].filter(Boolean);
|
|
369
|
+
for (const cookie of setCookies) {
|
|
370
|
+
const match = /^coulomb_sess=([^;]+)/.exec(cookie);
|
|
371
|
+
if (match) {
|
|
372
|
+
this._coulombToken = match[1] ?? null;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (response.status === 401) {
|
|
377
|
+
throw new InvalidSession(401, "ChargePoint session expired. Please log in again.");
|
|
378
|
+
}
|
|
379
|
+
if (response.status === 403) {
|
|
380
|
+
let body;
|
|
381
|
+
try {
|
|
382
|
+
body = await response.clone().json();
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
const captchaUrl = body?.url;
|
|
386
|
+
if (typeof captchaUrl === "string" && captchaUrl.includes("datadome")) {
|
|
387
|
+
throw new DatadomeCaptcha(captchaUrl);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return response;
|
|
391
|
+
}
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// Authentication
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
async loginWithPassword(password) {
|
|
396
|
+
const url = `${this.globalConfig.endpoints.ssoEndpoint}/v1/user/login`;
|
|
397
|
+
const response = await this._request("POST", url, {
|
|
398
|
+
body: JSON.stringify({ user_name: this._username, password }),
|
|
399
|
+
headers: { "Content-Type": "application/json" }
|
|
400
|
+
});
|
|
401
|
+
if (!response.ok) {
|
|
402
|
+
let body;
|
|
403
|
+
try {
|
|
404
|
+
body = await response.json();
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
throw new LoginError(response.status, "Failed to login with password.", body);
|
|
408
|
+
}
|
|
409
|
+
await response.text();
|
|
410
|
+
if (!this._coulombToken) {
|
|
411
|
+
throw new LoginError(response.status, "Login succeeded but no session token was returned.");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async loginWithSsoSession(ssoJwt) {
|
|
415
|
+
const url = `${this.globalConfig.endpoints.portalDomainEndpoint}/index.php/nghelper/getSession`;
|
|
416
|
+
const response = await this._request("GET", url, {
|
|
417
|
+
headers: {
|
|
418
|
+
cookie: `auth-session=${ssoJwt}`,
|
|
419
|
+
"Content-Type": "application/json"
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
if (!response.ok) {
|
|
423
|
+
let body;
|
|
424
|
+
try {
|
|
425
|
+
body = await response.json();
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
throw new LoginError(response.status, "Failed to login with SSO session.", body);
|
|
429
|
+
}
|
|
430
|
+
await response.text();
|
|
431
|
+
}
|
|
432
|
+
async logout() {
|
|
433
|
+
const url = `${this.globalConfig.endpoints.ssoEndpoint}/v1/user/logout`;
|
|
434
|
+
try {
|
|
435
|
+
await this._request("POST", url);
|
|
436
|
+
} finally {
|
|
437
|
+
this._coulombToken = null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Account
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
async getAccount() {
|
|
444
|
+
const url = `${this.globalConfig.endpoints.accountsEndpoint}/v1/driver/profile/user`;
|
|
445
|
+
const response = await this._request("GET", url);
|
|
446
|
+
if (!response.ok) {
|
|
447
|
+
throw new CommunicationError(response.status, "Failed to get account information.");
|
|
448
|
+
}
|
|
449
|
+
const data = await response.json();
|
|
450
|
+
if (data.user?.userId) {
|
|
451
|
+
this._userId = data.user.userId;
|
|
452
|
+
}
|
|
453
|
+
return data;
|
|
454
|
+
}
|
|
455
|
+
async getVehicles() {
|
|
456
|
+
const url = `${this.globalConfig.endpoints.accountsEndpoint}/v1/driver/vehicle`;
|
|
457
|
+
const response = await this._request("GET", url);
|
|
458
|
+
if (!response.ok) {
|
|
459
|
+
throw new CommunicationError(response.status, "Failed to get vehicles.");
|
|
460
|
+
}
|
|
461
|
+
const data = await response.json();
|
|
462
|
+
return Array.isArray(data.vehicles) ? data.vehicles : [];
|
|
463
|
+
}
|
|
464
|
+
async getUserChargingStatus() {
|
|
465
|
+
const url = `${this.globalConfig.endpoints.mapcacheEndpoint}/v2`;
|
|
466
|
+
const response = await this._request("POST", url, {
|
|
467
|
+
body: JSON.stringify({ user_status: { timestamp: Date.now() } }),
|
|
468
|
+
headers: { "Content-Type": "application/json" }
|
|
469
|
+
});
|
|
470
|
+
if (!response.ok) {
|
|
471
|
+
throw new CommunicationError(response.status, "Failed to get user charging status.");
|
|
472
|
+
}
|
|
473
|
+
const data = await response.json();
|
|
474
|
+
const userStatus = data.user_status;
|
|
475
|
+
if (!userStatus) return null;
|
|
476
|
+
const charging = userStatus.charging_status;
|
|
477
|
+
if (!charging) return null;
|
|
478
|
+
const stations = Array.isArray(charging.stations) ? charging.stations.map((s) => ({
|
|
479
|
+
id: s.id,
|
|
480
|
+
name: typeof s.name === "string" ? s.name : "",
|
|
481
|
+
latitude: typeof s.lat === "number" ? s.lat : s.latitude ?? 0,
|
|
482
|
+
longitude: typeof s.lon === "number" ? s.lon : s.longitude ?? 0
|
|
483
|
+
})) : [];
|
|
484
|
+
return {
|
|
485
|
+
sessionId: charging.session_id,
|
|
486
|
+
startTime: parseMsTimestamp2(charging.start_time),
|
|
487
|
+
state: typeof charging.current_charging === "string" ? charging.current_charging : "",
|
|
488
|
+
stations
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// Home charger helpers
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
async ensureUserId() {
|
|
495
|
+
if (this._userId !== null) return this._userId;
|
|
496
|
+
const account = await this.getAccount();
|
|
497
|
+
return account.user.userId;
|
|
498
|
+
}
|
|
499
|
+
async getHomeChargers() {
|
|
500
|
+
const userId = await this.ensureUserId();
|
|
501
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/configuration/users/${userId}/chargers`;
|
|
502
|
+
const response = await this._request("GET", url);
|
|
503
|
+
if (!response.ok) {
|
|
504
|
+
let body;
|
|
505
|
+
try {
|
|
506
|
+
body = await response.json();
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
throw new CommunicationError(response.status, "Failed to get home chargers.", body);
|
|
510
|
+
}
|
|
511
|
+
const data = await response.json();
|
|
512
|
+
const arr = Array.isArray(data.data) ? data.data : Array.isArray(data.chargers) ? data.chargers : [];
|
|
513
|
+
return arr.map((c) => {
|
|
514
|
+
const id = c.id ?? c.chargerId ?? c.charger_id;
|
|
515
|
+
return typeof id === "number" ? id : Number(id);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
async getHomeChargerStatus(chargerId) {
|
|
519
|
+
const userId = await this.ensureUserId();
|
|
520
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/configuration/users/${userId}/chargers/${chargerId}/status`;
|
|
521
|
+
const response = await this._request("GET", url);
|
|
522
|
+
if (!response.ok) {
|
|
523
|
+
throw new CommunicationError(response.status, "Failed to get home charger status.");
|
|
524
|
+
}
|
|
525
|
+
const data = await response.json();
|
|
526
|
+
const amp = data.chargeAmperageSettings ?? {};
|
|
527
|
+
return {
|
|
528
|
+
chargerId,
|
|
529
|
+
// not echoed by the API; inject from parameter
|
|
530
|
+
brand: String(data.brand ?? ""),
|
|
531
|
+
model: String(data.model ?? ""),
|
|
532
|
+
macAddress: String(data.macAddress ?? ""),
|
|
533
|
+
chargingStatus: String(data.chargingStatus ?? ""),
|
|
534
|
+
isPluggedIn: Boolean(data.isPluggedIn),
|
|
535
|
+
isConnected: Boolean(data.isConnected),
|
|
536
|
+
isReminderEnabled: Boolean(data.isReminderEnabled),
|
|
537
|
+
plugInReminderTime: String(data.plugInReminderTime ?? ""),
|
|
538
|
+
hasUtilityInfo: Boolean(data.hasUtilityInfo),
|
|
539
|
+
isDuringScheduledTime: Boolean(data.isDuringScheduledTime),
|
|
540
|
+
amperageLimit: Number(amp.chargeLimit ?? data.amperageLimit ?? 0),
|
|
541
|
+
possibleAmperageLimits: (amp.possibleChargeLimit ?? data.possibleAmperageLimits ?? []).map(Number)
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
async getHomeChargerTechnicalInfo(chargerId) {
|
|
545
|
+
const userId = await this.ensureUserId();
|
|
546
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/configuration/users/${userId}/chargers/${chargerId}/technical-info`;
|
|
547
|
+
const response = await this._request("GET", url);
|
|
548
|
+
if (!response.ok) {
|
|
549
|
+
throw new CommunicationError(response.status, "Failed to get home charger technical info.");
|
|
550
|
+
}
|
|
551
|
+
return await response.json();
|
|
552
|
+
}
|
|
553
|
+
async getHomeChargerConfig(chargerId) {
|
|
554
|
+
const userId = await this.ensureUserId();
|
|
555
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/configuration/users/${userId}/chargers/${chargerId}/configurations`;
|
|
556
|
+
const response = await this._request("GET", url);
|
|
557
|
+
if (!response.ok) {
|
|
558
|
+
throw new CommunicationError(response.status, "Failed to get home charger configuration.");
|
|
559
|
+
}
|
|
560
|
+
const data = await response.json();
|
|
561
|
+
const s = data.settings ?? data;
|
|
562
|
+
const rawLed = s.led?.brightness ?? s.ledBrightness ?? {};
|
|
563
|
+
const level = typeof rawLed.level === "string" ? Number(rawLed.level) : Number(rawLed.level ?? rawLed.currentBrightnessSettings ?? 0);
|
|
564
|
+
const supportedLevels = Array.isArray(rawLed.supportedLevels) ? rawLed.supportedLevels.map(Number) : [];
|
|
565
|
+
return {
|
|
566
|
+
serialNumber: String(s.serialNumber ?? ""),
|
|
567
|
+
macAddress: String(s.macAddress ?? ""),
|
|
568
|
+
stationNickname: String(s.stationNickname ?? ""),
|
|
569
|
+
streetAddress: String(s.streetAddress ?? ""),
|
|
570
|
+
hasUtilityInfo: Boolean(s.hasUtilityInfo),
|
|
571
|
+
utility: s.utility ?? null,
|
|
572
|
+
// API may return boolean true/false or string "ON"/"OFF"
|
|
573
|
+
indicatorLightEcoMode: s.indicatorLightEcoMode === true || s.indicatorLightEcoMode === "ON",
|
|
574
|
+
flashlightReset: Boolean(s.flashlightReset),
|
|
575
|
+
worksWithNest: Boolean(s.worksWithNest),
|
|
576
|
+
isPairedWithNest: Boolean(s.isPairedWithNest),
|
|
577
|
+
isInstalledByInstaller: Boolean(s.isInstalledByInstaller),
|
|
578
|
+
ledBrightness: {
|
|
579
|
+
level,
|
|
580
|
+
inProgress: Boolean(rawLed.inProgress),
|
|
581
|
+
supportedLevels,
|
|
582
|
+
isEnabled: rawLed.isEnabled !== false
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async getHomeChargerSchedule(chargerId) {
|
|
587
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/schedule/charger/${chargerId}/schedule`;
|
|
588
|
+
const response = await this._request("GET", url);
|
|
589
|
+
if (!response.ok) {
|
|
590
|
+
throw new CommunicationError(response.status, "Failed to get home charger schedule.");
|
|
591
|
+
}
|
|
592
|
+
return await response.json();
|
|
593
|
+
}
|
|
594
|
+
async setHomeChargerSchedule(chargerId, weekdayStart, weekdayEnd, weekendStart, weekendEnd) {
|
|
595
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/schedule/charger/${chargerId}/schedule`;
|
|
596
|
+
const response = await this._request("PUT", url, {
|
|
597
|
+
body: JSON.stringify({
|
|
598
|
+
scheduleEnabled: true,
|
|
599
|
+
userSchedule: {
|
|
600
|
+
weekdays: { startTime: weekdayStart, endTime: weekdayEnd },
|
|
601
|
+
weekends: { startTime: weekendStart, endTime: weekendEnd }
|
|
602
|
+
}
|
|
603
|
+
}),
|
|
604
|
+
headers: { "Content-Type": "application/json" }
|
|
605
|
+
});
|
|
606
|
+
if (!response.ok) {
|
|
607
|
+
throw new CommunicationError(response.status, "Failed to set home charger schedule.");
|
|
608
|
+
}
|
|
609
|
+
return await response.json();
|
|
610
|
+
}
|
|
611
|
+
async disableHomeChargerSchedule(chargerId) {
|
|
612
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/schedule/charger/${chargerId}/schedule`;
|
|
613
|
+
const response = await this._request("PUT", url, {
|
|
614
|
+
body: JSON.stringify({ scheduleEnabled: false }),
|
|
615
|
+
headers: { "Content-Type": "application/json" }
|
|
616
|
+
});
|
|
617
|
+
if (!response.ok) {
|
|
618
|
+
throw new CommunicationError(response.status, "Failed to disable home charger schedule.");
|
|
619
|
+
}
|
|
620
|
+
await response.text();
|
|
621
|
+
}
|
|
622
|
+
async setAmperageLimit(chargerId, amperageLimit) {
|
|
623
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/configuration/chargers/${chargerId}/charge-amperage-limit`;
|
|
624
|
+
const response = await this._request("PUT", url, {
|
|
625
|
+
body: JSON.stringify({ amperageLimit }),
|
|
626
|
+
headers: { "Content-Type": "application/json" }
|
|
627
|
+
});
|
|
628
|
+
if (!response.ok) {
|
|
629
|
+
throw new CommunicationError(response.status, "Failed to set amperage limit.");
|
|
630
|
+
}
|
|
631
|
+
await response.text();
|
|
632
|
+
}
|
|
633
|
+
async setLedBrightness(chargerId, level) {
|
|
634
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/configuration/chargers/${chargerId}/led-brightness`;
|
|
635
|
+
const response = await this._request("PUT", url, {
|
|
636
|
+
body: JSON.stringify({ level }),
|
|
637
|
+
headers: { "Content-Type": "application/json" }
|
|
638
|
+
});
|
|
639
|
+
if (!response.ok) {
|
|
640
|
+
throw new CommunicationError(response.status, "Failed to set LED brightness.");
|
|
641
|
+
}
|
|
642
|
+
await response.text();
|
|
643
|
+
}
|
|
644
|
+
async restartHomeCharger(chargerId) {
|
|
645
|
+
const userId = await this.ensureUserId();
|
|
646
|
+
const url = `${this.globalConfig.endpoints.hcpoHcmEndpoint}/api/v1/configuration/users/${userId}/chargers/${chargerId}/restart`;
|
|
647
|
+
const response = await this._request("POST", url);
|
|
648
|
+
if (!response.ok) {
|
|
649
|
+
throw new CommunicationError(response.status, "Failed to restart home charger.");
|
|
650
|
+
}
|
|
651
|
+
await response.text();
|
|
652
|
+
}
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
// Charging sessions
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
async getChargingSession(sessionId) {
|
|
657
|
+
const session = new ChargingSession(sessionId);
|
|
658
|
+
session._setClient(this);
|
|
659
|
+
await session.refresh();
|
|
660
|
+
return session;
|
|
661
|
+
}
|
|
662
|
+
async startChargingSession(deviceId) {
|
|
663
|
+
return ChargingSession.start(deviceId, this);
|
|
664
|
+
}
|
|
665
|
+
// ---------------------------------------------------------------------------
|
|
666
|
+
// Stations
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
668
|
+
async getStation(deviceId) {
|
|
669
|
+
const url = `${this.globalConfig.endpoints.mapcacheEndpoint}/v3/station/info?deviceId=${deviceId}&use_cache=false`;
|
|
670
|
+
const response = await this._request("GET", url);
|
|
671
|
+
if (!response.ok) {
|
|
672
|
+
throw new CommunicationError(response.status, "Failed to get station info.");
|
|
673
|
+
}
|
|
674
|
+
return await response.json();
|
|
675
|
+
}
|
|
676
|
+
async getNearbyStations(bounds, filter = {}) {
|
|
677
|
+
const url = `${this.globalConfig.endpoints.mapcacheEndpoint}/v2`;
|
|
678
|
+
const stationList = {
|
|
679
|
+
ne_lat: bounds.neLat,
|
|
680
|
+
ne_lon: bounds.neLon,
|
|
681
|
+
sw_lat: bounds.swLat,
|
|
682
|
+
sw_lon: bounds.swLon,
|
|
683
|
+
...filter
|
|
684
|
+
};
|
|
685
|
+
const response = await this._request("POST", url, {
|
|
686
|
+
body: JSON.stringify({ station_list: stationList }),
|
|
687
|
+
headers: { "Content-Type": "application/json" }
|
|
688
|
+
});
|
|
689
|
+
if (!response.ok) {
|
|
690
|
+
throw new CommunicationError(response.status, "Failed to get nearby stations.");
|
|
691
|
+
}
|
|
692
|
+
const data = await response.json();
|
|
693
|
+
const list = data.station_list;
|
|
694
|
+
return Array.isArray(list?.stations) ? list.stations : [];
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
export { APIError, ChargePoint, ChargingSession, CommunicationError, DatadomeCaptcha, InvalidSession, LoginError };
|
|
699
|
+
//# sourceMappingURL=index.js.map
|
|
700
|
+
//# sourceMappingURL=index.js.map
|