iobroker.psa
Version:
PSA Adapter for Peugeot, Citroen, DS, Opel
630 lines (607 loc) • 22.7 kB
JavaScript
"use strict";
/*
* Created with @iobroker/create-adapter v1.34.0
*/
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
const utils = require("@iobroker/adapter-core");
const axios = require("axios").default;
const https = require("https");
const Json2iob = require("json2iob");
const fs = require("fs");
// const crypto = require("crypto");
class Psa extends utils.Adapter {
/**
* @param {Partial<utils.AdapterOptions>} [options={}]
*/
constructor(options) {
super({
...options,
name: "psa",
});
this.on("ready", this.onReady.bind(this));
this.on("unload", this.onUnload.bind(this));
this.requestClient = axios.create({
withCredentials: true,
timeout: 3 * 60 * 1000, //3min client timeout
});
this.json2iob = new Json2iob(this);
this.idArray = [];
this.brands = {
peugeot: {
brand: "peugeot.com",
realm: "clientsB2CPeugeot",
clientId: "1eebc2d5-5df3-459b-a624-20abfcf82530",
basic: "MWVlYmMyZDUtNWRmMy00NTliLWE2MjQtMjBhYmZjZjgyNTMwOlQ1dFA3aVMwY084c0MwbEEyaUUyYVI3Z0s2dUU1ckYzbEo4cEMzbk8xcFI3dEw4dlUx",
siteCode: "AP_DE_ESP",
shortBrand: "AP",
url: "mw-ap-rp.mym.awsmpsa.com",
redirectUri: "mymap://oauth2redirect/de",
},
citroen: {
brand: "citroen.com",
realm: "clientsB2CCitroen",
clientId: "5364defc-80e6-447b-bec6-4af8d1542cae",
basic: "NTM2NGRlZmMtODBlNi00NDdiLWJlYzYtNGFmOGQxNTQyY2FlOmlFMGNEOGJCMHlKMGRTNnJPM25OMWhJMndVN3VBNXhSNGdQN2xENnZNMG9IMG5TOGRO",
siteCode: "AC_DE_ESP",
shortBrand: "AC",
url: "mw-ac-rp.mym.awsmpsa.com",
redirectUri: "mymacsdk://oauth2redirect/de", //mymacsdk
},
driveds: {
brand: "driveds.com",
realm: "clientsB2CDS",
clientId: "cbf74ee7-a303-4c3d-aba3-29f5994e2dfa",
basic: "Y2JmNzRlZTctYTMwMy00YzNkLWFiYTMtMjlmNTk5NGUyZGZhOlg2YkU2eVEzdEgxY0c1b0E2YVc0ZlM2aEswY1IwYUs1eU4yd0U0aFA4dkw4b1c1Z1Uz",
siteCode: "DS_DE_ESP",
shortBrand: "DS",
url: "mw-ds-rp.mym.awsmpsa.com",
redirectUri: "mymdssdk://oauth2redirect/de",
},
opel: {
brand: "opel.com",
realm: "clientsB2COpel",
clientId: "07364655-93cb-4194-8158-6b035ac2c24c",
basic: "MDczNjQ2NTUtOTNjYi00MTk0LTgxNTgtNmIwMzVhYzJjMjRjOkYya0s3bEM1a0Y1cU43dE0wd1Q4a0UzY1cxZFAwd0M1cEk2dkMwc1E1aVA1Y044Y0o4",
siteCode: "OP_DE_ESP",
shortBrand: "OP",
url: "mw-op-rp.mym.awsmpsa.com",
redirectUri: "mymop://oauth2redirect/de",
},
};
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
// Initialize your adapter here
this.setState("info.connection", false, true);
if (!this.config.type) {
this.log.warn("Please select type in settings");
return;
}
if (this.config.interval < 0.5) {
this.log.info("Set interval to minimum 0.5");
this.config.interval = 0.5;
}
this.clientId = this.brands[this.config.type].clientId;
this.httpsAgent = new https.Agent({
pfx: fs.readFileSync(__dirname + "/certs/mwp.dat"),
passphrase: "y5Y2my5B",
});
await this.extendObject("auth", {
type: "channel",
common: {
name: "Authentification",
},
native: {},
});
await this.extendObject("auth.session", {
type: "state",
common: {
name: "Session",
type: "string",
role: "value",
read: true,
write: true,
},
native: {},
});
this.session = {};
const sessionState = await this.getStateAsync("auth.session");
if (sessionState) {
this.session = JSON.parse(sessionState.val);
if (this.session.refresh_token) {
this.log.info("Found old session. Try to refresh token");
await this.refreshToken();
}
} else {
if (this.config.auth_code) {
this.log.info("Found auth code. Try to login");
await this.loginAuthCode();
} else {
this.log.warn("Please enter authorization code in settings");
}
}
this.log.info("Get vehicles");
await this.getVehicles();
this.log.info("Get vehicle status");
await this.updateVehicle();
this.appUpdateInterval = setInterval(() => {
this.updateVehicle();
}, this.config.interval * 60 * 1000);
this.refreshTokenInterval = setInterval(() => {
this.refreshToken();
}, 60 * 60 * 1000 - 150);
// try {
// this.receiveOldApi()
// .then(() => {
// this.log.info("OldAPI Login succesful, but only mileage is available");
// this.oldApiUpdateInterval = setInterval(() => {
// this.receiveOldApi().catch((_error) => {
// this.log.warn("OldAPI Status failed");
// });
// }, this.config.interval * 60 * 1000);
// })
// .catch((_error) => {
// this.log.warn("OldAPI Login failed, only relevant for non eletric cars");
// });
// } catch (error) {
// this.log.error(error);
// }
// await this.loginNewApi();
// if (this.newApi && this.newApi.mym_access_token) {
// this.getnewApiData().then(() => {
// this.log.info("Receive Data from new API you can find it under psa.0.newApi");
// });
// this.newApiUpdateInterval = setInterval(() => {
// this.getnewApiData();
// }, this.config.interval * 60 * 1000);
// }
}
async loginAuthCode() {
//check if auth code is a url and extract code or if it is a code
if (this.config.auth_code.includes("code=")) {
this.config.auth_code = new URL(this.config.auth_code).searchParams.get("code");
}
await axios({
method: "post",
url: "https://idpcvs." + this.brands[this.config.type].brand + "/am/oauth2/access_token",
headers: {
"User-Agent": "okhttp/4.10.0",
"Content-Type": "application/x-www-form-urlencoded",
Authorization: "Basic " + this.brands[this.config.type].basic,
Accept: "application/json",
},
data: {
realm: this.brands[this.config.type].realm,
grant_type: "authorization_code",
code: this.config.auth_code,
redirect_uri: this.brands[this.config.type].redirectUri,
},
})
.then((response) => {
if (!response.data) {
this.log.error("Login failed maybe incorrect login information");
return;
}
this.log.info("Login succesful. Save session for relogin");
this.log.debug(JSON.stringify(response.data));
this.session = response.data;
this.setState("auth.session", JSON.stringify(response.data), true);
this.setState("info.connection", true, true);
})
.catch((error) => {
this.log.error(error);
this.log.error("Login failed");
error.response && this.log.error(JSON.stringify(error.response.data));
this.config.auth_code = "";
this.log.error("Renew authorization code");
});
}
async updateVehicle() {
// this.idArray.forEach((element) => {
for (const element of this.idArray) {
await this.getRequest(
"https://api.groupe-psa.com/connectedcar/v4/user/vehicles/" + element.id + "/status",
element.vin + ".status",
).catch(() => {
this.log.error("Get device status failed");
this.log.info("Remove device " + element.vin + " from list");
const index = this.idArray.indexOf(element);
if (index !== -1) {
this.idArray.splice(index, 1);
}
});
}
}
// async loginNewApi() {
// let data = JSON.stringify({
// fields: { USR_PASSWORD: { value: this.config.password }, USR_EMAIL: { value: this.config.user } },
// action: "authenticate",
// siteCode: this.brands[this.config.type].siteCode,
// culture: "de-DE",
// });
// const access_token = await axios({
// method: "post",
// url: "https://id-dcr." + this.brands[this.config.type].brand + "/mobile-services/GetAccessToken",
// headers: {
// Accept: "*/*",
// Cookie: "PSACountry=DE",
// "User-Agent": "MyPeugeot/1.35.2 (iPhone; iOS 12.5.1; Scale/2.00)",
// "Accept-Language": "de-DE;q=1",
// "Content-Type": "application/x-www-form-urlencoded",
// },
// data: "jsonRequest=" + encodeURIComponent(data),
// })
// .then(async (response) => {
// if (!response.data) {
// this.log.error("Login old api failed maybe incorrect login information");
// return;
// }
// this.log.debug(JSON.stringify(response.data));
// if (!response.data.accessToken) {
// this.log.warn(JSON.stringify(response.data));
// if (response.data && response.data.returnCode && response.data.returnCode === "NEED_CREATION") {
// this.log.warn("No account for this e-mail or password incorrect");
// }
// if (response.data.returnCode === "NEED_AUTHORIZATION") {
// this.log.info("new API needs Auth if this is failing please logout and login in the app");
// } else {
// this.log.warn("No Token received for old api ");
// return;
// }
// } else {
// return response.data.accessToken;
// }
// })
// .catch((error) => {
// this.log.warn(error);
// this.log.warn("Login new api failed");
// error.response && this.log.warn(JSON.stringify(error.response.data));
// });
// if (!access_token) {
// return;
// }
// data = JSON.stringify({ site_code: this.brands[this.config.type].siteCode, ticket: this.oldAToken });
// this.log.debug(data);
// await axios({
// method: "get",
// url:
// "https://microservices.mym.awsmpsa.com/session/v1/accesstoken?source=APP&v=1.35.2&site_code=" +
// this.brands[this.config.type].siteCode +
// "&language=de&brand=" +
// this.brands[this.config.type].shortBrand +
// "&culture=de_DE",
// headers: {
// Host: "microservices.mym.awsmpsa.com",
// ticket: access_token,
// accept: "*/*",
// "user-agent": "MyPeugeot/1.35.2 (com.psa.mypeugeot; build:202206081500; iOS 14.8.0) Alamofire/5.6.1",
// "accept-language": "de-DE;q=1.0",
// },
// httpsAgent: this.httpsAgent,
// })
// .then(async (response) => {
// this.log.debug(JSON.stringify(response.data));
// if (response.data.success) {
// await this.setObjectNotExistsAsync("newApi", {
// type: "device",
// common: {
// name: "new API, only mileage available",
// role: "indicator",
// },
// native: {},
// });
// this.newApi = response.data.success;
// }
// })
// .catch((error) => {
// this.log.warn(error);
// this.log.warn("receive new api failed");
// error.response && this.log.warn(JSON.stringify(error.response.data));
// });
// }
async getnewApiData() {
if (this.newApi && !this.newApi.mym_access_token) {
return;
}
await axios({
method: "get",
url:
"https://microservices.mym.awsmpsa.com/me/v1/user?source=APP&v=1.35.2&site_code=" +
this.brands[this.config.type].siteCode +
"&language=de&brand=" +
this.brands[this.config.type].shortBrand +
"&culture=de_DE",
headers: {
Host: "microservices.mym.awsmpsa.com",
accept: "*/*",
"mym-access-token": this.newApi.mym_access_token,
"refresh-sams-cache": "1",
"user-agent": "MyPeugeot/1.35.2 (com.psa.mypeugeot; build:202206081500; iOS 14.8.0) Alamofire/5.6.1",
"accept-language": "de-DE;q=1.0",
},
httpsAgent: this.httpsAgent,
})
.then((response) => {
this.log.debug(JSON.stringify(response.data));
if (response.data.success) {
this.json2iob.parse("newApi", response.data.success);
}
})
.catch((error) => {
this.log.warn(error);
error.response && this.log.warn(JSON.stringify(error.response.data));
});
}
receiveOldApi() {
return new Promise((resolve, reject) => {
const data = JSON.stringify({
fields: { USR_PASSWORD: { value: this.config.password }, USR_EMAIL: { value: this.config.user } },
action: "authenticate",
siteCode: this.brands[this.config.type].siteCode,
culture: "de-DE",
});
axios({
method: "post",
url: "https://id-dcr." + this.brands[this.config.type].brand + "/mobile-services/GetAccessToken",
headers: {
Accept: "*/*",
Cookie:
"BIGipServerAPAC_DCR_FEND_PROD.app~APAC_DCR_FEND_PROD_pool=655392778.20480.0000; DCROPENIDAP=77f8a8f32fb2c2ef7692bb9afa996486; PSACountry=DE",
"User-Agent": "MyPeugeot/1.29.1 (iPhone; iOS 12.5.1; Scale/2.00)",
"Accept-Language": "de-DE;q=1",
"Content-Type": "application/x-www-form-urlencoded",
},
data: "jsonRequest=" + encodeURIComponent(data),
})
.then(async (response) => {
if (!response.data) {
this.log.error("Login old api failed maybe incorrect login information");
reject();
return;
}
this.log.debug(JSON.stringify(response.data));
if (!response.data.accessToken) {
this.log.warn(JSON.stringify(response.data));
if (response.data && response.data.returnCode && response.data.returnCode === "NEED_CREATION") {
this.log.warn("No account for this e-mail or password incorrect");
}
if (response.data.returnCode === "NEED_AUTHORIZATION") {
this.log.info("Old API needs Auth if this is failing please logout and login in the app");
this.oldSession = response.data.session;
this.oldToken = response.data.token;
await this.receiveOldApiAuth();
} else {
this.log.warn("No Token received for old api ");
return;
}
} else {
this.oldAToken = response.data.accessToken;
}
await this.setObjectNotExistsAsync("oldApi", {
type: "device",
common: {
name: "old API, only mileage available",
role: "indicator",
},
native: {},
});
const data = JSON.stringify({ site_code: this.brands[this.config.type].siteCode, ticket: this.oldAToken });
const url =
"https://" +
this.brands[this.config.type].url +
"/api/v1/user?culture=de_DE&width=1080&cgu=" +
parseInt(Date.now() / 1000) +
"&v=1.29.3";
this.log.debug(url);
this.log.debug(data);
axios({
method: "post",
url: url,
headers: {
"source-agent": "App-Android",
version: "1.29.3",
token: this.oldAToken,
"content-type": "application/json;charset=UTF-8",
"user-agent": "okhttp/4.9.1",
},
data: data,
httpsAgent: this.httpsAgent,
})
.then((response) => {
this.log.debug(JSON.stringify(response.data));
if (response.data.success) {
this.json2iob.parse("oldApi", response.data.success);
}
resolve();
})
.catch((error) => {
this.log.warn(error);
this.log.warn("receive old api failed");
error.response && this.log.warn(JSON.stringify(error.response.data));
reject();
});
})
.catch((error) => {
this.log.warn(error);
this.log.warn("Login old api failed");
error.response && this.log.warn(JSON.stringify(error.response.data));
reject();
});
});
}
receiveOldApiAuth() {
return new Promise((resolve, reject) => {
const data = JSON.stringify({
action: "authorize",
siteCode: this.brands[this.config.type].siteCode,
session: this.oldSession,
token: this.oldToken,
culture: "de-DE",
});
axios({
method: "post",
url: "https://id-dcr." + this.brands[this.config.type].brand + "/mobile-services/GetAccessToken",
headers: {
"User-Agent": "MyPeugeot/1.29.3 (iPhone; iOS 14.7; Scale/2.00)",
"Accept-Language": "de-DE;q=1",
"Content-Type": "application/x-www-form-urlencoded",
},
data: "jsonRequest=" + encodeURIComponent(data),
})
.then(async (response) => {
if (!response.data) {
this.log.error("Auth old api failed maybe incorrect login information");
reject();
return;
}
this.log.debug(JSON.stringify(response.data));
if (!response.data.accessToken) {
this.log.warn(JSON.stringify(response.data));
this.log.warn("No Auth Token received for old api ");
return;
}
this.oldAToken = response.data.accessToken;
resolve();
})
.catch((error) => {
this.log.warn(error);
this.log.warn("Auth old api failed");
error.response && this.log.warn(JSON.stringify(error.response.data));
reject();
});
});
}
async refreshToken() {
await axios({
method: "post",
url: "https://idpcvs." + this.brands[this.config.type].brand + "/am/oauth2/access_token",
headers: {
accept: "application/json",
"User-Agent": "okhttp/4.10.0",
"Content-Type": "application/x-www-form-urlencoded",
Authorization: "Basic " + this.brands[this.config.type].basic,
},
data: {
realm: this.brands[this.config.type].realm,
grant_type: "refresh_token",
refresh_token: this.session.refresh_token,
},
})
.then((response) => {
this.log.debug("Refresh Token succesful");
this.log.debug(JSON.stringify(response.data));
this.session = response.data;
this.setState("auth.session", JSON.stringify(response.data), true);
this.setState("info.connection", true, true);
})
.catch((error) => {
this.setState("info.connection", false, true);
this.log.error(error);
this.log.error("Refreshtoken failed. Please delete auth.session and restart adapter");
error.response && this.log.error(JSON.stringify(error.response.data));
});
}
async getVehicles() {
await axios({
method: "get",
url: "https://api.groupe-psa.com/connectedcar/v4/user",
params: { client_id: this.clientId },
headers: {
Authorization: "Bearer " + this.session.access_token,
Accept: "application/hal+json",
"x-introspect-realm": this.brands[this.config.type].realm,
},
})
.then(async (response) => {
this.log.debug(JSON.stringify(response.data));
this.json2iob.parse("user", response.data);
this.log.info("Found " + response.data["_embedded"].vehicles.length + " vehicles");
for (const element of response.data["_embedded"].vehicles) {
this.idArray.push({ id: element.id, vin: element.vin });
await this.extendObject(element.vin, {
type: "device",
common: {
name: element.vin,
role: "indicator",
},
native: {},
});
await this.getRequest("https://api.groupe-psa.com/connectedcar/v4/user/vehicles/" + element.id, element.vin + ".details").catch(
() => {
this.log.error("Get Details failed");
},
);
}
})
.catch((error) => {
this.log.error(error);
error.response && this.log.error(JSON.stringify(error.response.data));
if (error.response && error.response.data && error.response.data.code && error.response.data.code === 40410) {
this.log.error("No compatible vehicles found. Only electric cars are available for new api.");
this.log.info("You can find under oldAPI the mileage of the car.");
}
});
}
async getRequest(url, path) {
this.log.debug(url + "?client_id=" + this.clientId);
await axios({
method: "get",
url: url,
params: { client_id: this.clientId },
headers: {
Authorization: "Bearer " + this.session.access_token,
Accept: "application/hal+json",
"x-introspect-realm": this.brands[this.config.type].realm,
},
})
.then((response) => {
this.log.debug(JSON.stringify(response.data));
this.json2iob.parse(path, response.data, { preferedArrayName: "type" });
})
.catch((error) => {
if (error.response && error.response.status === 401) {
this.log.info("Token expired. Try to refresh token");
this.setState("info.connection", false, true);
this.refreshToken();
return;
}
this.log.error(error);
this.log.error("Get " + path + " failed");
error.response && this.log.error(JSON.stringify(error.response.data));
});
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
* @param {() => void} callback
*/
async onUnload(callback) {
this.clearInterval(this.appUpdateInterval);
this.clearInterval(this.oldApiUpdateInterval);
this.clearInterval(this.refreshTokenInterval);
this.newApiUpdateInterval && this.clearInterval(this.newApiUpdateInterval);
if (this.config.auth_code) {
const adapterSettings = await this.getForeignObjectAsync("system.adapter." + this.namespace);
adapterSettings.native.auth_code = null;
await this.setForeignObjectAsync("system.adapter." + this.namespace, adapterSettings);
}
try {
callback();
} catch (e) {
this.log.error("Unload failed: " + e);
callback();
}
}
}
if (require.main !== module) {
// Export the constructor in compact mode
/**
* @param {Partial<utils.AdapterOptions>} [options={}]
*/
module.exports = (options) => new Psa(options);
} else {
// otherwise start the instance directly
new Psa();
}