Added tronity car connector
MODIFY
src/integration/car-connector/CarConnectorFactory.ts
src/integration/car-connector/CarConnectorFactory.ts
-import { CarConnectorConnectionType, CarConnectorSetting } from '../../types/Setting';
+import { CarConnectorConnectionType, CarConnectorSettings } from '../../types/Setting';
+import Tenant, { TenantComponents } from '../../types/Tenant';
import CarConnectorIntegration from './CarConnectorIntegration';
import MercedesCarConnectorIntegration from './mercedes-connector/MercedesCarConnectorIntegration';
import SettingStorage from '../../storage/mongodb/SettingStorage';
-import Tenant from '../../types/Tenant';
-import { TenantComponents } from '../../types/Tenant';
+import TronityCarConnectorIntegration from './tronity-connector/TronityCarConnectorIntegration';
import Utils from '../../utils/Utils';
export default class CarConnectorFactory {
- static async getCarConnectorImpl(tenant: Tenant, connectorId: string): Promise<CarConnectorIntegration<CarConnectorSetting>> {
+ static async getCarConnectorImpl(tenant: Tenant, connectorId: string): Promise<CarConnectorIntegration<CarConnectorSettings>> {
// Check if car connector component is active
if (Utils.isTenantComponentActive(tenant, TenantComponents.CAR_CONNECTOR)) {
const settings = await SettingStorage.getCarConnectorSettings(tenant);
@@ -17,10 +17,13 @@ export default class CarConnectorFactory {
// Vehicle make will be replaced with specific car information in the car object --> coming with Tronity
const foundConnection = settings.carConnector.connections.find((connection) => connection.type === connectorId);
if (foundConnection) {
- let carConnectorIntegrationImpl: CarConnectorIntegration<CarConnectorSetting> = null;
+ let carConnectorIntegrationImpl: CarConnectorIntegration<CarConnectorSettings> = null;
switch (foundConnection.type) {
case CarConnectorConnectionType.MERCEDES:
- carConnectorIntegrationImpl = new MercedesCarConnectorIntegration(tenant, settings.carConnector, foundConnection);
+ carConnectorIntegrationImpl = new MercedesCarConnectorIntegration(tenant, settings, foundConnection);
+ break;
+ case CarConnectorConnectionType.TRONITY:
+ carConnectorIntegrationImpl = new TronityCarConnectorIntegration(tenant, settings, foundConnection);
break;
}
return carConnectorIntegrationImpl;

MODIFY
src/integration/car-connector/CarConnectorIntegration.ts
src/integration/car-connector/CarConnectorIntegration.ts
-import { CarConnectorConnectionSetting, CarConnectorSetting } from '../../types/Setting';
+import { CarConnectorConnectionSetting, CarConnectorConnectionToken, CarConnectorSettings } from '../../types/Setting';
import { Car } from '../../types/Car';
import Connection from '../../types/Connection';
import Tenant from '../../types/Tenant';
+import Utils from '../../utils/Utils';
-export default abstract class CarConnectorIntegration<T extends CarConnectorSetting> {
+export default abstract class CarConnectorIntegration<T extends CarConnectorSettings> {
protected readonly tenant: Tenant;
protected settings: T;
protected connection: CarConnectorConnectionSetting;
@@ -15,7 +16,15 @@ export default abstract class CarConnectorIntegration<T extends CarConnectorSett
this.connection = connection;
}
- public abstract createConnection(userID: string, data: unknown): Promise<Connection>;
+ public checkIfTokenExpired(token: CarConnectorConnectionToken): boolean {
+ if (!Utils.isNullOrUndefined(token)) {
+ const expireTime = new Date(token.expires).getTime() - 60000; // 1 minute buffer
+ const now = new Date().getTime();
+ return expireTime <= now;
+ }
+ return true;
+ }
public abstract getCurrentSoC(userID: string, car: Car): Promise<number>;
+
}

MODIFY
src/integration/car-connector/mercedes-connector/MercedesCarConnectorIntegration.ts
src/integration/car-connector/mercedes-connector/MercedesCarConnectorIntegration.ts
import { AxiosInstance, AxiosResponse } from 'axios';
-import { CarConnectorConnectionSetting, CarConnectorConnectionType, CarConnectorSetting } from '../../../types/Setting';
+import { CarConnectorConnectionSetting, CarConnectorConnectionType, CarConnectorSettings } from '../../../types/Setting';
import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
import AxiosFactory from '../../../utils/AxiosFactory';
@@ -19,7 +19,7 @@ import querystring from 'querystring';
const MODULE_NAME = 'MercedesCarConnectorIntegration';
-export default class MercedesCarConnectorIntegration extends CarConnectorIntegration<CarConnectorSetting> {
+export default class MercedesCarConnectorIntegration extends CarConnectorIntegration<CarConnectorSettings> {
private axiosInstance: AxiosInstance;
private readonly axiosRetryConfiguration: IAxiosRetryConfig = {
retries: 3,
@@ -69,7 +69,7 @@ export default class MercedesCarConnectorIntegration extends CarConnectorIntegra
shouldResetTimeout: true
};
- constructor(tenant: Tenant, settings: CarConnectorSetting, connection: CarConnectorConnectionSetting) {
+ constructor(tenant: Tenant, settings: CarConnectorSettings, connection: CarConnectorConnectionSetting) {
super(tenant, settings, connection);
// Get Axios
this.axiosInstance = AxiosFactory.getAxiosInstance(this.tenant.id,

ADD
src/integration/car-connector/tronity-connector/TronityCarConnectorIntegration.ts
+import { CarConnectorConnectionSetting, CarConnectorConnectionToken, CarConnectorSettings } from '../../../types/Setting';
+import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
+
+import AxiosFactory from '../../../utils/AxiosFactory';
+import { AxiosInstance } from 'axios';
+import BackendError from '../../../exception/BackendError';
+import { Car } from '../../../types/Car';
+import CarConnectorIntegration from '../CarConnectorIntegration';
+import Constants from '../../../utils/Constants';
+import Cypher from '../../../utils/Cypher';
+import Logging from '../../../utils/Logging';
+import { ServerAction } from '../../../types/Server';
+import SettingStorage from '../../../storage/mongodb/SettingStorage';
+import { StatusCodes } from 'http-status-codes';
+import Tenant from '../../../types/Tenant';
+import Utils from '../../../utils/Utils';
+
+const MODULE_NAME = 'TronityCarConnectorIntegration';
+
+export default class TronityCarConnectorIntegration extends CarConnectorIntegration<CarConnectorSettings> {
+ private axiosInstance: AxiosInstance;
+ private readonly axiosRetryConfiguration: IAxiosRetryConfig = {
+ retries: 3,
+ retryCondition: (error) => error.response.status === StatusCodes.INTERNAL_SERVER_ERROR || axiosRetry.isNetworkError(error),
+ retryDelay: (retryCount, error) => {
+ try {
+ if (error.config.method === 'post') {
+ if (error.config.url.endsWith('/token.oauth2')) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME,
+ method: 'retryDelay',
+ message: `Unable to post token, response status ${error.response.status}, attempt ${retryCount}`,
+ action: ServerAction.CAR_CONNECTOR,
+ detailedMessages: { response: error.response }
+ });
+ } else {
+ const payload = {
+ error: error.response.data,
+ payload: JSON.parse(error.config.data)
+ };
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME,
+ method: 'retryDelay',
+ message: `Unable to post data on ${error.config.url}, response status ${error.response.status}, attempt ${retryCount}`,
+ action: ServerAction.CAR_CONNECTOR,
+ detailedMessages: { payload }
+ });
+ }
+ } else {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME,
+ method: 'retryDelay',
+ message: `Unable to make data request on ${error.config.url}, response status ${error.response.status}, attempt ${retryCount}`,
+ action: ServerAction.CAR_CONNECTOR,
+ detailedMessages: { response: error.response.data }
+ });
+ }
+ } catch (err) {
+ void Logging.logException(
+ err, ServerAction.CAR_CONNECTOR, Constants.CENTRAL_SERVER, MODULE_NAME, 'anonymous', this.tenant.id, null);
+ }
+ return axiosRetry.exponentialDelay(retryCount);
+ },
+ shouldResetTimeout: true
+ };
+
+ constructor(tenant: Tenant, settings: CarConnectorSettings, connection: CarConnectorConnectionSetting) {
+ super(tenant, settings, connection);
+ // Get Axios
+ this.axiosInstance = AxiosFactory.getAxiosInstance(this.tenant.id,
+ {
+ axiosRetryConfig: this.axiosRetryConfiguration,
+ });
+ }
+
+ public async connect(): Promise<string> {
+ if (!this.checkIfTokenExpired(this.connection.token)) {
+ return this.connection.token.accessToken;
+ }
+ // Check if connection is initialized
+ this.checkConnectionIsProvided();
+ // Get credential params
+ const credentials = await this.getCredentialURLParams();
+ const response = await this.fetchNewToken(credentials);
+ return response.accessToken;
+ }
+
+ public async getCurrentSoC(userID: string, car: Car): Promise<number> {
+ const connectionToken = await this.connect();
+ const request = `${this.connection.tronityConnection.apiUrl}/`;
+ try {
+ // Get consumption
+ const response = await this.axiosInstance.get(
+ request,
+ {
+ headers: { 'Authorization': 'Bearer ' + connectionToken }
+ }
+ );
+ await Logging.logDebug({
+ tenantID: this.tenant.id,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.CAR_CONNECTOR,
+ message: `${car.vin} > Tronity web service has been called successfully`,
+ module: MODULE_NAME, method: 'getCurrentSoC',
+ detailedMessages: { response: response.data }
+ });
+ if (response?.data?.soc?.value) {
+ return response.data.soc.value;
+ }
+ return null;
+ } catch (error) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME,
+ method: 'getCurrentSoC',
+ action: ServerAction.CAR_CONNECTOR,
+ message: 'Error while retrieving the SOC',
+ detailedMessages: { request, error: error.stack }
+ });
+ }
+ }
+
+
+ private async fetchNewToken(credentials: URLSearchParams) {
+ const response = await Utils.executePromiseWithTimeout(5000,
+ this.axiosInstance.post(`${this.connection.tronityConnection.apiUrl}/oauth/authentication`,
+ credentials,
+ {
+ 'axios-retry': {
+ retries: 0
+ },
+ headers: this.buildFormHeaders()
+ }),
+ `Time out error (5s) when getting the token with the connection URL '${this.connection.tronityConnection.apiUrl}/oauth/authentication'`
+ );
+ const data = response.data;
+ const token : CarConnectorConnectionToken = {
+ accessToken: data.access_token,
+ tokenType: data.token_type,
+ expiresIn: data.expires_in,
+ userName: data.userName,
+ issued: data['.issued'],
+ expires: data['.expires']
+ };
+ this.connection.token = token;
+ await SettingStorage.saveSettings(this.tenant, this.settings);
+ return token;
+ }
+
+ private buildFormHeaders(): any {
+ return {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ };
+ }
+
+ private checkConnectionIsProvided(): void {
+ if (!this.connection) {
+ throw new BackendError({
+ source: Constants.CENTRAL_SERVER,
+ module: MODULE_NAME,
+ method: 'checkConnectionIsProvided',
+ action: ServerAction.CHECK_CONNECTION,
+ message: 'No connection provided'
+ });
+ }
+ }
+
+ private async getCredentialURLParams(): Promise<URLSearchParams> {
+ const params = new URLSearchParams();
+ params.append('grant_type', 'app');
+ params.append('client_id', this.connection.tronityConnection.clientId);
+ params.append('client_secret', await Cypher.decrypt(this.tenant, this.connection.tronityConnection.clientSecret));
+ return params;
+ }
+}

MODIFY
src/server/rest/v1/service/security/SettingSecurity.ts
src/server/rest/v1/service/security/SettingSecurity.ts
@@ -276,6 +276,13 @@ export default class SettingSecurity {
clientSecret: sanitize(connection.mercedesConnection.clientSecret),
};
break;
+ case CarConnectorConnectionType.TRONITY:
+ sanitizedConnection.tronityConnection = {
+ apiUrl: sanitize(connection.tronityConnection.apiUrl),
+ clientId: sanitize(connection.tronityConnection.clientId),
+ clientSecret: sanitize(connection.tronityConnection.clientSecret),
+ };
+ break;
}
settings.content.carConnector.connections.push(sanitizedConnection);
}

MODIFY
src/types/Setting.ts
src/types/Setting.ts
@@ -333,12 +333,15 @@ export interface CarConnectorConnectionSetting {
description: string;
timestamp: Date;
type: CarConnectorConnectionType;
+ token?: CarConnectorConnectionToken;
mercedesConnection?: CarConnectorMercedesConnectionType;
+ tronityConnection?: CarConnectorTronityConnectionType;
}
export enum CarConnectorConnectionType {
NONE = '',
MERCEDES = 'mercedes',
+ TRONITY = 'tronity'
}
export interface CarConnectorMercedesConnectionType {
@@ -348,6 +351,21 @@ export interface CarConnectorMercedesConnectionType {
clientSecret: string;
}
+export interface CarConnectorTronityConnectionType {
+ apiUrl: string;
+ clientId: string;
+ clientSecret: string;
+}
+
+export interface CarConnectorConnectionToken {
+ accessToken: string,
+ tokenType?: string,
+ expiresIn?: number,
+ userName?: string,
+ issued?: Date,
+ expires: Date,
+}
+
export enum CryptoSettingsType {
CRYPTO = 'crypto'
}

Added tronity car connector