Last active
January 24, 2024 21:53
-
-
Save MaXeraph/0c393e15d4b3d17fc998949716faa975 to your computer and use it in GitHub Desktop.
polestar-medium-widget-frankenstein.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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}`); | |
} |
I keep getting 224:22: No battery data fetched
any ideas?
Seems like the VIN might be wrong?
I have double checked that, no luck
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I keep getting 224:22: No battery data fetched
any ideas?