UNPKG

nodejs-connected-drive

Version:
219 lines (218 loc) 14.9 kB
"use strict"; 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); };