Skip to content

Instantly share code, notes, and snippets.

@MaXeraph
Last active January 24, 2024 21:53
Show Gist options
  • Save MaXeraph/0c393e15d4b3d17fc998949716faa975 to your computer and use it in GitHub Desktop.
Save MaXeraph/0c393e15d4b3d17fc998949716faa975 to your computer and use it in GitHub Desktop.
polestar-medium-widget-frankenstein.js
// icon-color: green; icon-glyph: battery-half;
/**
* This widget has been developed by Niklas Vieth.
* Installation and configuration details can be found at https://github.com/niklasvieth/polestar-ios-medium-widget
*/
// Config
const MIN_SOC_GREEN = 60;
const MIN_SOC_ORANGE = 20;
const POLESTAR_EMAIL = "EMAIL_ADDRESS"; //EDIT THIS
const POLESTAR_PASSWORD = "PASSWORD"; //EDIT THIS
const VIN = "VIN"; //EDIT THIS
const POLESTAR_BASE_URL = "https://pc-api.polestar.com/eu-north-1";
const POLESTAR_API_URL = `${POLESTAR_BASE_URL}/mystar-v2`; // v1 API: "https://pc-api.polestar.com/eu-north-1/my-star"
const POLESTAR_ICON = "https://www.polestar.com/w3-assets/coast-228x228.png";
// Check that params are set
if (POLESTAR_EMAIL === "EMAIL_ADDRESS") {
throw new Error("Parameter POLESTAR_EMAIL is not configured");
}
if (POLESTAR_PASSWORD === "PASSWORD") {
throw new Error("Parameter POLESTAR_PASSWORD is not configured");
}
if (VIN === "VIN") {
throw new Error("Parameter VIN is not configured");
}
const accessToken = await getAccessToken();
const batteryData = await getBattery(accessToken);
const batteryPercent = parseInt(batteryData.batteryChargeLevelPercentage);
const isCharging = batteryData.chargingStatus === "CHARGING_STATUS_CHARGING";
const isChargingDone = batteryData.chargingStatus === "CHARGING_STATUS_DONE";
const isConnected =
batteryData.chargerConnectionStatus === "CHARGER_CONNECTION_STATUS_CONNECTED";
const inputData = {
name: "2024 Polestar 2 Magnesium",
battery: {
percent: batteryPercent,
isCharging: isCharging,
isChargingDone: isChargingDone,
isConnected: isConnected},
imgUrl: "https://cas.polestar.com/image/dynamic/MY24_2335/534/summary-transparent-v1/FD/1/39/72900/RFA000/R184/LR01/JT02/BD02/EV05/JB11/2G03/ET01/default.png?market=en&width=1000&angle=1&bg=00000000"
} //EDIT THIS
// You can run the script in the app to preview the widget or you can go to the Home Screen, add a new Scriptable widget and configure the widget to run this script.
// You can also try creating a shortcut that runs this script. Running the shortcut will show widget.
// const inputData = await fetchinputData();
const widget = await createPolestarWidget(inputData);
if (config.runsInWidget) {
// The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
Script.setWidget(widget);
} else {
// The script runs inside the app, so we preview the widget.
widget.presentMedium();
}
Script.complete();
// Create polestar widget
async function createPolestarWidget(inputData) {
const appIcon = await loadImage(POLESTAR_ICON);
const title = inputData.name;
const widget = new ListWidget();
widget.url = "polestar-explore://";
// Add background gradient
const gradient = new LinearGradient();
gradient.locations = [0, 1];
gradient.colors = [new Color("141414"), new Color("13233F")];
widget.backgroundGradient = gradient;
// Show app icon and title
const titleStack = widget.addStack();
const titleElement = titleStack.addText(title);
titleElement.textColor = Color.white();
titleElement.textOpacity = 0.7;
titleElement.font = Font.mediumSystemFont(14);
titleStack.addSpacer();
const appIconElement = titleStack.addImage(appIcon);
appIconElement.imageSize = new Size(30, 30);
appIconElement.cornerRadius = 4;
widget.addSpacer(12);
// Center Stack
const contentStack = widget.addStack();
const carImage = await loadImage(inputData.imgUrl);
const carImageElement = contentStack.addImage(carImage);
carImageElement.imageSize = new Size(150, 100);
contentStack.addSpacer();
// Battery Info
const batteryInfoStack = contentStack.addStack();
batteryInfoStack.layoutVertically();
batteryInfoStack.addSpacer();
// Battery Percent Value
const batteryPercent = inputData.battery.percent;
const isCharging = inputData.battery.isCharging;
const batteryPercentStack = batteryInfoStack.addStack();
batteryPercentStack.addSpacer();
batteryPercentStack.centerAlignContent();
const batterySymbol = getBatteryPercentIcon(batteryPercent, isCharging);
const batterySymbolElement = batteryPercentStack.addImage(
batterySymbol.image
);
batterySymbolElement.imageSize = new Size(40, 40);
batterySymbolElement.tintColor = getBatteryPercentColor(batteryPercent);
batteryPercentStack.addSpacer(8);
const batteryPercentText = batteryPercentStack.addText(`${batteryPercent} %`);
batteryPercentText.textColor = getBatteryPercentColor(batteryPercent);
batteryPercentText.font = Font.boldSystemFont(24);
// Footer
const footerStack = batteryInfoStack.addStack();
footerStack.addSpacer();
return widget;
}
/**********************
* Polestar API helpers
**********************/
async function getAccessToken() {
const { pathToken, cookie } = await getLoginFlowTokens();
const tokenRequestCode = await performLogin(pathToken, cookie);
const apiCreds = await getApiToken(tokenRequestCode);
return apiCreds.access_token;
}
async function performLogin(pathToken, cookie) {
const req = new Request(
`https://polestarid.eu.polestar.com/as/${pathToken}/resume/as/authorization.ping`
);
req.method = "post";
req.body = getUrlEncodedParams({
"pf.username": POLESTAR_EMAIL,
"pf.pass": POLESTAR_PASSWORD,
});
req.headers = {
"Content-Type": "application/x-www-form-urlencoded",
Cookie: cookie,
};
req.onRedirect = (redReq) => {
return null;
};
await req.load();
const redirectUrl = req.response.headers.Location;
const regex = /code=([^&]+)/;
const match = redirectUrl.match(regex);
const tokenRequestCode = match ? match[1] : null;
return tokenRequestCode;
}
async function getLoginFlowTokens() {
const req = new Request(
"https://polestarid.eu.polestar.com/as/authorization.oauth2?response_type=code&client_id=polmystar&redirect_uri=https://www.polestar.com%2Fsign-in-callback&scope=openid+profile+email+customer%3Aattributes"
);
req.headers = { Cookie: "" };
let redirectUrl;
req.onRedirect = (redReq) => {
redirectUrl = redReq.url;
return null;
};
await req.loadString();
const regex = /resumePath=(\w+)/;
const match = redirectUrl.match(regex);
const pathToken = match ? match[1] : null;
const cookies = req.response.headers["Set-Cookie"];
const cookie = cookies.split("; ")[0] + ";";
return {
pathToken: pathToken,
cookie: cookie,
};
}
async function getApiToken(tokenRequestCode) {
const req = new Request(`${POLESTAR_BASE_URL}/auth`);
req.method = "POST";
req.headers = {
"Content-Type": "application/json",
};
req.body = JSON.stringify({
query:
"query getAuthToken($code: String!){getAuthToken(code: $code){id_token,access_token,refresh_token,expires_in}}",
operationName: "getAuthToken",
variables: { code: tokenRequestCode },
});
req.onRedirect = (redReq) => {
return null;
};
const response = await req.loadJSON();
const apiCreds = response.data.getAuthToken;
return {
access_token: apiCreds.access_token,
refresh_token: apiCreds.refresh_token,
expires_in: apiCreds.expires_in,
};
}
async function getBattery(accessToken) {
if (!accessToken) {
throw new Error("Not authenticated");
}
const searchParams = {
query:
"query GetBatteryData($vin:String!){getBatteryData(vin:$vin){averageEnergyConsumptionKwhPer100Km,batteryChargeLevelPercentage,chargerConnectionStatus,chargingCurrentAmps,chargingPowerWatts,chargingStatus,estimatedChargingTimeMinutesToTargetDistance,estimatedChargingTimeToFullMinutes,estimatedDistanceToEmptyKm,estimatedDistanceToEmptyMiles,eventUpdatedTimestamp{iso,unix}}}",
variables: {
vin: VIN,
},
};
const req = new Request(POLESTAR_API_URL);
req.method = "POST";
req.headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + accessToken,
};
req.body = JSON.stringify(searchParams);
const response = await req.loadJSON();
if (!response?.data?.getBatteryData) {
throw new Error("No battery data fetched");
}
const data = response.data.getBatteryData;
return data;
}
async function loadImage(url) {
const req = new Request(url);
return req.loadImage();
}
function getUrlEncodedParams(object) {
return Object.keys(object)
.map((key) => `${key}=${encodeURIComponent(object[key])}`)
.join("&");
}
/*************
* Formatters
*************/
function getBatteryPercentColor(percent) {
if (percent > MIN_SOC_GREEN) {
return Color.green();
} else if (percent > MIN_SOC_ORANGE) {
return Color.orange();
} else {
return Color.red();
}
}
function getBatteryPercentIcon(percent, isCharging) {
if (isCharging) {
return SFSymbol.named(`battery.100percent.bolt`);
}
let percentRounded = 0;
if (percent > 90) {
percentRounded = 100;
} else if (percent > 60) {
percentRounded = 75;
} else if (percent > 40) {
percentRounded = 50;
} else if (percent > 10) {
percentRounded = 25;
}
return SFSymbol.named(`battery.${percentRounded}`);
}
@matt3man
Copy link

I keep getting 224:22: No battery data fetched

any ideas?

@MaXeraph
Copy link
Author

I keep getting 224:22: No battery data fetched

any ideas?

Seems like the VIN might be wrong?

@matt3man
Copy link

matt3man commented Jan 24, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment