bluelinky2
Version:
An unofficial nodejs API wrapper for Hyundai bluelink
360 lines (324 loc) • 11.5 kB
text/typescript
import got from 'got';
import logger from '../logger';
import {
REGIONS,
DEFAULT_VEHICLE_STATUS_OPTIONS,
ChargeTarget,
POSSIBLE_CHARGE_LIMIT_VALUES,
} from '../constants';
import {
VehicleStartOptions,
VehicleLocation,
VehicleRegisterOptions,
VehicleStatus,
VehicleOdometer,
VehicleStatusOptions,
RawVehicleStatus,
FullVehicleStatus,
EVChargeModeTypes,
} from '../interfaces/common.interfaces';
import { Vehicle } from './vehicle';
import { celciusToTempCode, parseDate } from '../util';
import { CanadianController } from '../controllers/canadian.controller';
import { ManagedBluelinkyError } from '../tools/common.tools';
export default class CanadianVehicle extends Vehicle {
public region = REGIONS.CA;
private timeOffset = -(new Date().getTimezoneOffset() / 60);
constructor(public vehicleConfig: VehicleRegisterOptions, public controller: CanadianController) {
super(vehicleConfig, controller);
logger.debug(`CA Vehicle ${this.vehicleConfig.id} created`);
}
public fullStatus(): Promise<FullVehicleStatus | null> {
throw new Error('Method not implemented.');
}
public async status(
input: VehicleStatusOptions
): Promise<VehicleStatus | RawVehicleStatus | null> {
const statusConfig = {
...DEFAULT_VEHICLE_STATUS_OPTIONS,
...input,
};
logger.debug('Begin status request, polling car: ' + input.refresh);
try {
const endpoint = statusConfig.refresh
? this.controller.environment.endpoints.remoteStatus
: this.controller.environment.endpoints.status;
const response = await this.request(endpoint, {});
const vehicleStatus = response.result?.status;
if (response?.error) {
throw response?.error?.errorDesc;
}
logger.debug(vehicleStatus);
const parsedStatus: VehicleStatus = {
chassis: {
hoodOpen: vehicleStatus?.hoodOpen,
trunkOpen: vehicleStatus?.trunkOpen,
locked: vehicleStatus?.doorLock,
openDoors: {
frontRight: !!vehicleStatus?.doorOpen?.frontRight,
frontLeft: !!vehicleStatus?.doorOpen?.frontLeft,
backLeft: !!vehicleStatus?.doorOpen?.backLeft,
backRight: !!vehicleStatus?.doorOpen?.backRight,
},
tirePressureWarningLamp: {
rearLeft: !!vehicleStatus?.tirePressureLamp?.tirePressureWarningLampRearLeft,
frontLeft: !!vehicleStatus?.tirePressureLamp?.tirePressureWarningLampFrontLeft,
frontRight: !!vehicleStatus?.tirePressureLamp?.tirePressureWarningLampFrontRight,
rearRight: !!vehicleStatus?.tirePressureLamp?.tirePressureWarningLampRearRight,
all: !!vehicleStatus?.tirePressureLamp?.tirePressureWarningLampAll,
},
},
climate: {
active: vehicleStatus?.airCtrlOn,
steeringwheelHeat: !!vehicleStatus?.steerWheelHeat,
sideMirrorHeat: false,
rearWindowHeat: !!vehicleStatus?.sideBackWindowHeat,
defrost: vehicleStatus?.defrost,
temperatureSetpoint: vehicleStatus?.airTemp?.value,
temperatureUnit: vehicleStatus?.airTemp?.unit,
},
// TODO: fix props for parsed???
// Seems some of the translation would have to account for EV and ICE
// as they are often in different locations on the response
// example EV status is in lib/__mock__/canadianStatus.json
engine: {
ignition: vehicleStatus?.engine,
accessory: vehicleStatus?.acc,
range: vehicleStatus?.dte?.value,
charging: vehicleStatus?.evStatus?.batteryCharge,
batteryCharge12v: vehicleStatus?.battery?.batSoc,
batteryChargeHV: vehicleStatus?.evStatus?.batteryStatus,
},
lastupdate: vehicleStatus?.time ? parseDate(vehicleStatus?.lastStatusDate) : null,
};
this._status = statusConfig.parsed ? parsedStatus : vehicleStatus;
return this._status;
} catch (err) {
throw err.message;
}
}
//////////////////////////////////////////////////////////////////////////////
// Car commands with preauth (PIN)
//////////////////////////////////////////////////////////////////////////////
public async lock(): Promise<string> {
logger.debug('Begin lock request');
try {
const preAuth = await this.getPreAuth();
// assuming the API returns a bad status code for failed attempts
await this.request(this.controller.environment.endpoints.lock, {}, { pAuth: preAuth });
return 'Lock successful';
} catch (err) {
throw err.message;
}
}
public async unlock(): Promise<string> {
logger.debug('Begin unlock request');
try {
const preAuth = await this.getPreAuth();
await this.request(this.controller.environment.endpoints.unlock, {}, { pAuth: preAuth });
return 'Unlock successful';
} catch (err) {
throw err.message;
}
}
/*
airCtrl: Boolean, // climatisation
heating1: Boolean, // front defrost, airCtrl will be on
defrost: Boolean, // side mirrors & rear defrost
airTempvalue: number | null // temp in degrees for clim and heating 17-27
*/
public async start(startConfig: VehicleStartOptions): Promise<string> {
logger.debug('Begin startClimate request');
try {
const body = {
hvacInfo: {
airCtrl: (startConfig.airCtrl ?? false) || (startConfig.defrost ?? false) ? 1 : 0,
defrost: startConfig.defrost ?? false,
// postRemoteFatcStart: 1,
heating1: startConfig.heating1 ? 1 : 0,
},
};
const airTemp = startConfig.airTempvalue;
// TODO: can we use getTempCode here from util?
if (airTemp != null) {
body.hvacInfo['airTemp'] = {
value: celciusToTempCode(REGIONS.CA, airTemp),
unit: 0,
hvacTempType: 1,
};
} else if ((startConfig.airCtrl ?? false) || (startConfig.defrost ?? false)) {
throw 'air temperature should be specified';
}
const preAuth = await this.getPreAuth();
const response = await this.request(this.controller.environment.endpoints.start, body, {
pAuth: preAuth,
});
logger.debug(response);
if (response.responseHeader && response.responseHeader.responseCode === 0) {
return 'Vehicle started!';
}
return 'Failed to start vehicle';
} catch (err) {
throw err.message;
}
}
public async stop(): Promise<string> {
logger.debug('Begin stop request');
try {
const preAuth = await this.getPreAuth();
const response = await this.request(this.controller.environment.endpoints.stop, {
pAuth: preAuth,
});
return response;
} catch (err) {
throw 'error: ' + err;
}
}
// TODO: type this
public async lights(withHorn = false): Promise<string> {
logger.debug('Begin lights request with horn ' + withHorn);
try {
const preAuth = await this.getPreAuth();
const response = await this.request(
this.controller.environment.endpoints.hornlight,
{ horn: withHorn },
{ pAuth: preAuth }
);
return response;
} catch (err) {
throw 'error: ' + err;
}
}
/**
* Warning only works on EV vehicles
* @returns
*/
public async stopCharge(): Promise<void> {
logger.debug('Begin stopCharge');
const { stopCharge } = this.controller.environment.endpoints;
try {
const preAuth = await this.getPreAuth();
const response = await this.request(stopCharge, {
pin: this.controller.userConfig.pin,
pAuth: preAuth,
});
return response;
} catch (err) {
throw 'error: ' + err;
}
}
/**
* Warning only works on EV vehicles
* @returns
*/
public async startCharge(): Promise<void> {
logger.debug('Begin startCharge');
const { startCharge } = this.controller.environment.endpoints;
try {
const preAuth = await this.getPreAuth();
const response = await this.request(startCharge, {
pin: this.controller.userConfig.pin,
pAuth: preAuth,
});
return response;
} catch (err) {
throw 'error: ' + err;
}
}
/**
* Warning only works on EV vehicles
* @param limits
* @returns Promise<void>
*/
public async setChargeTargets(limits: { fast: ChargeTarget; slow: ChargeTarget }): Promise<void> {
logger.debug('Begin setChargeTarget');
if (
!POSSIBLE_CHARGE_LIMIT_VALUES.includes(limits.fast) ||
!POSSIBLE_CHARGE_LIMIT_VALUES.includes(limits.slow)
) {
throw new ManagedBluelinkyError(
`Charge target values are limited to ${POSSIBLE_CHARGE_LIMIT_VALUES.join(', ')}`
);
}
const { setChargeTarget } = this.controller.environment.endpoints;
try {
const preAuth = await this.getPreAuth();
const response = await this.request(setChargeTarget, {
pin: this.controller.userConfig.pin,
pAuth: preAuth,
tsoc: [
{ plugType: EVChargeModeTypes.FAST, level: limits.fast },
{ plugType: EVChargeModeTypes.SLOW, level: limits.slow },
],
});
return response;
} catch (err) {
throw 'error: ' + err;
}
}
// TODO: @Seb to take a look at doing this
public odometer(): Promise<VehicleOdometer | null> {
throw new Error('Method not implemented.');
}
public async location(): Promise<VehicleLocation> {
logger.debug('Begin locate request');
try {
const preAuth = await this.getPreAuth();
const response = await this.request(
this.controller.environment.endpoints.locate,
{},
{ pAuth: preAuth }
);
this._location = response.result as VehicleLocation;
return this._location;
} catch (err) {
throw 'error: ' + err;
}
}
//////////////////////////////////////////////////////////////////////////////
// Internal
//////////////////////////////////////////////////////////////////////////////
// Does this have to be done before every command?
private async getPreAuth(): Promise<string> {
logger.info('Begin pre-authentication');
try {
const response = await this.request(this.controller.environment.endpoints.verifyPin, {});
return response.result.pAuth;
} catch (err) {
throw 'error: ' + err;
}
}
// TODO: not sure how to type a dynamic response
/* eslint-disable @typescript-eslint/no-explicit-any */
private async request(endpoint, body: any, headers: any = {}): Promise<any | null> {
logger.debug(`[${endpoint}] ${JSON.stringify(headers)} ${JSON.stringify(body)}`);
// add logic for token refresh to ensure we don't use a stale token
await this.controller.refreshAccessToken();
const options = {
method: 'POST',
json: true,
throwHttpErrors: false,
headers: {
from: this.controller.environment.origin,
language: 1,
offset: this.timeOffset,
accessToken: this.controller.session.accessToken,
vehicleId: this.vehicleConfig.id,
...headers,
},
body: {
pin: this.userConfig.pin,
...body,
},
};
try {
const response: any = await got(endpoint, options);
if (response.body.responseHeader.responseCode != 0) {
return response.body.responseHeader.responseDesc;
}
return response.body;
} catch (err) {
throw 'error: ' + err;
}
}
}