nodejs-connected-drive
Version:
NodeJS client for BMW Connected Drive
219 lines (218 loc) • 14.9 kB
JavaScript
;
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _ConnectedDrive_instances, _ConnectedDrive_configuration, _ConnectedDrive_username, _ConnectedDrive_password, _ConnectedDrive_sessionExpiresAt, _ConnectedDrive_accessToken, _ConnectedDrive_httpRequest, _ConnectedDrive_getRemoteServiceStatus, _ConnectedDrive_waitUntil;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConnectedDrive = void 0;
const querystring = require("querystring");
const deepMerge_1 = require("./misc/deepMerge");
const RemoteServiceCommand_1 = require("./enums/RemoteServiceCommand");
const RemoteServiceExecutionState_1 = require("./enums/RemoteServiceExecutionState");
const url_1 = require("url");
const default_js_1 = require("./config/default.js");
const got_1 = require("got");
/**
* SDK class that expose the Connected Drive API.
*
* Note that `login()` does not need to be called explicitly.
* The SDK will lazily call `login()` to (re)authenticate when necessary.
*/
class ConnectedDrive {
/** The generic type parameter should be either omitted or `false` in production. */
constructor(
/** Required. The Connected Drive username */
username,
/** Required. The Connected Drive username */
password,
/** Optional. Override the default configuration. */
configuration) {
_ConnectedDrive_instances.add(this);
_ConnectedDrive_configuration.set(this, void 0);
_ConnectedDrive_username.set(this, void 0);
_ConnectedDrive_password.set(this, void 0);
_ConnectedDrive_sessionExpiresAt.set(this, void 0);
_ConnectedDrive_accessToken.set(this, void 0);
__classPrivateFieldSet(this, _ConnectedDrive_configuration, (!configuration ? default_js_1.default : (0, deepMerge_1.deepMerge)(default_js_1.default, configuration)), "f");
__classPrivateFieldSet(this, _ConnectedDrive_username, username, "f");
__classPrivateFieldSet(this, _ConnectedDrive_password, password, "f");
}
/** Authenticate with the Connected Drive API and store the resulting access_token on 'this'. This function is also called lazily by the SDK when necessary. */
async login() {
const { connectedDrive: { auth } } = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f");
const response = await (0, got_1.default)(new url_1.URL(auth.endpoints.authenticate, auth.host).href, {
method: "POST",
followRedirect: false,
form: {
client_id: auth.client_id,
redirect_uri: auth.redirect_uri,
response_type: auth.response_type,
scope: auth.scope,
username: __classPrivateFieldGet(this, _ConnectedDrive_username, "f"),
password: __classPrivateFieldGet(this, _ConnectedDrive_password, "f"),
state: auth.state
}
});
if (!response.headers.location) {
throw new Error("Expected the Location header to be defined");
}
const queryStringFromHash = new url_1.URL(response.headers.location).hash.slice(1);
const { access_token, expires_in } = querystring.parse(queryStringFromHash);
__classPrivateFieldSet(this, _ConnectedDrive_sessionExpiresAt, new (__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").clock.Date)(__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").clock.Date.now() + parseInt(expires_in) * 1000), "f");
__classPrivateFieldSet(this, _ConnectedDrive_accessToken, access_token, "f");
__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").logger.info("Successfully authenticated with the Connected Drive API");
}
/** Returns a list specifying the physical configuration of vehicles. */
async getVehicles() {
const path = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").connectedDrive.endpoints.getVehicles;
return await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_httpRequest).call(this, { path });
}
/** Returns details describing the Connected Drive services supported by the vehicle. */
async getVehicleDetails(vehicleVin) {
const path = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").connectedDrive.endpoints.getVehicleDetails
.replace("{vehicleVin}", vehicleVin);
return await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_httpRequest).call(this, { path });
}
/** Returns technical details of the vehicle such as milage, fuel reserve and service messages. */
async getVehicleTechnicalDetails(vehicleVin) {
const path = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").connectedDrive.endpoints.getVehicleTechnicalDetails
.replace("{vehicleVin}", vehicleVin);
return await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_httpRequest).call(this, { path });
}
/** Returns a list of vehicles detailing their connectivity and Connected Drive service status. */
async getStatusOfAllVehicles() {
const path = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").connectedDrive.endpoints.getStatusOfAllVehicles;
return await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_httpRequest).call(this, { path });
}
/**
* Execute a Connected Drive remote service. This may throw if:
* - specified vin isn't registered on the user
* - remote services aren't activated on the car
* - the car isn't online
* - the car-user relation hasn't been confirmed
* - the remote service takes too long time to complete. Default timeout 1 min.
* - if a new remote service command is sent to the car before this action has completed.
*/
async executeRemoteService(vehicleVin, service) {
const before = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").clock.Date.now();
const vehicleDetailsPath = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").connectedDrive.endpoints.getStatusOfAllVehicles
.replace("{vehicleVin}", vehicleVin);
const { vehicleRelationship } = await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_httpRequest).call(this, { path: vehicleDetailsPath });
const foundVehicle = vehicleRelationship.find(({ vin }) => vehicleVin === vin);
if (!foundVehicle) {
throw new Error(`Incorrect vehicle vin specified: '${vehicleVin}'. Found: ${JSON.stringify(vehicleRelationship.map(({ vin }) => vin))}`);
}
if (foundVehicle.remoteServiceStatus !== "ACTIVE") {
throw new Error(`The 'Remote Service' capability does not seem to be activated for vehicle ${vehicleVin}. Service status: ${foundVehicle.remoteServiceStatus}`);
}
if (foundVehicle.connectivityStatus !== "ACTIVE") {
throw new Error(`Vehicle ${vehicleVin} does not seem to be online. Connectivity status: ${foundVehicle.connectivityStatus}`);
}
if (foundVehicle.relationshipStatus !== "CONFIRMED") {
throw new Error(`The user account does not seem to be a recognized owner of Vehicle ${vehicleVin}. Relationship status: ${foundVehicle.relationshipStatus}`);
}
const path = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").connectedDrive.endpoints.executeRemoteServices
.replace("{vehicleVin}", vehicleVin)
.replace("{serviceType}", RemoteServiceCommand_1.RemoteServiceCommand[service]);
const { eventId: { eventId: triggeredEventId } } = await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_httpRequest).call(this, {
path,
method: "POST",
headers: { "Content-Type": "application/json;charset=UTF-8" },
body: "{}"
});
await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_waitUntil).call(this, async () => {
const status = await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_getRemoteServiceStatus).call(this, vehicleVin);
const { event: { eventId, rsEventStatus, actions } } = status;
if (triggeredEventId !== eventId) {
throw new Error("Event ID changed. Another operation is sent to the vehicle.");
}
// The actions list is not sorted by time by default.
// Sort the list to get the correct storyline in the log line below.
actions.sort((A, B) => new Date(A.creationTime).valueOf() - new Date(B.creationTime).valueOf());
__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").logger.debug(`Command ${service} executed actions: ${JSON.stringify(actions)}`);
const currentAction = actions.pop();
__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").logger.info(`Waiting for command ${service} to be executed on ${vehicleVin} ` +
`(eventId: ${eventId}, duration: ${__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").clock.Date.now() - before} ms): ` +
`${rsEventStatus} (${(currentAction === null || currentAction === void 0 ? void 0 : currentAction.rsDetailedStatus) || "null"})`);
return status.event.rsEventStatus === RemoteServiceExecutionState_1.RemoteServiceExecutionState.EXECUTED;
}, {
message: `Timed out awaiting Connected Drive to execute service ${service}`,
timeoutMs: __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").connectedDrive.remoteServiceExecutionTimeoutMs,
stepMs: __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").connectedDrive.pollIntervalMs
});
__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").logger.info(`${service} executed on ${vehicleVin} after ${__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").clock.Date.now() - before} ms`);
}
}
exports.ConnectedDrive = ConnectedDrive;
_ConnectedDrive_configuration = new WeakMap(), _ConnectedDrive_username = new WeakMap(), _ConnectedDrive_password = new WeakMap(), _ConnectedDrive_sessionExpiresAt = new WeakMap(), _ConnectedDrive_accessToken = new WeakMap(), _ConnectedDrive_instances = new WeakSet(), _ConnectedDrive_httpRequest =
/** Send a REST request to the Connected Drive API */
async function _ConnectedDrive_httpRequest({ path, method = "GET", body, forceLogin = false, headers }) {
if (forceLogin || !__classPrivateFieldGet(this, _ConnectedDrive_sessionExpiresAt, "f") || (__classPrivateFieldGet(this, _ConnectedDrive_sessionExpiresAt, "f").valueOf() - __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").clock.Date.now()) < 1000 * 60) {
await this.login();
}
if (!__classPrivateFieldGet(this, _ConnectedDrive_accessToken, "f")) {
throw new Error("No access token available");
}
const { connectedDrive: { host } } = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f");
try {
__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").logger.debug(`${method} ${host}${path} ${forceLogin ? "(retry)" : ""}`);
const response = await (0, got_1.default)(new url_1.URL(path, host).href, {
method,
headers: {
...headers,
authorization: `Bearer ${__classPrivateFieldGet(this, _ConnectedDrive_accessToken, "f")}`,
["user-agent"]: "nodejs-connected-drive",
},
body,
retry: {
limit: 2,
statusCodes: [404, 503, 504],
},
});
__classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").logger.debug(`${method} ${host}${path} response status ${response.statusCode}`, { body: JSON.parse(response.body) });
return JSON.parse(response.body);
}
catch (error) {
if (error instanceof got_1.default.HTTPError &&
error.response.statusCode === 401 &&
!forceLogin) {
return await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_httpRequest).call(this, {
path, method, body, forceLogin: true
});
}
if (error instanceof got_1.default.HTTPError) {
throw new Error(`HTTP ${method} ${path}: Status: ${error.response.statusCode}. Response body: ${error.response.rawBody.toString()}`);
}
else {
throw error;
}
}
}, _ConnectedDrive_getRemoteServiceStatus =
/** Poll the Connected Drive API for the current remote service execution status. */
async function _ConnectedDrive_getRemoteServiceStatus(vehicleVin) {
const path = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").connectedDrive.endpoints.statusRemoteServices
.replace("{vehicleVin}", vehicleVin);
return await __classPrivateFieldGet(this, _ConnectedDrive_instances, "m", _ConnectedDrive_httpRequest).call(this, { path });
}, _ConnectedDrive_waitUntil =
/** Helper function that fulfills its promise once the specified 'fn' return true. */
async function _ConnectedDrive_waitUntil(fn, { timeoutMs, message, stepMs = 1000 }) {
const { clock } = __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f");
const start = clock.Date.now();
while ((clock.Date.now() - start) < timeoutMs) {
if (await fn()) {
return;
}
else {
await new Promise(resolve => __classPrivateFieldGet(this, _ConnectedDrive_configuration, "f").clock.setTimeout(resolve, stepMs).unref());
}
}
throw new Error(message);
};