Hi,
Hier das Skript und die verwendeten Bilder. Die Bilder müssen in das scriptable Verzeichnis in der iCloud kopiert werden. Man sollte natürlich seine eigenen Bilder verwenden. Habe mal meine Bilder angehängt.
/**
* This version has been adapted by demichve
* Light background
* Small home wigdet
* Usage of Polestar API instead Tibber API
* Load of local pictures using iCloud
*/
// Config
const POLESTAR_EMAIL = "<your email>";
const POLESTAR_PASSWORD = "<your password>";
const VIN = "<your VIN>";
const POLESTAR_ICON = "star.png";
const POLESTAR_PIC = "polestar2.jpg";
let fm = FileManager.iCloud();
const iconpath = await loadicloud(POLESTAR_ICON);
const picpath = await loadicloud(POLESTAR_PIC);
// 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");
}
// 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 widget = await createPolestarWidget();
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.presentSmall();
}
Script.complete();
// Create Widget
async function createPolestarWidget() {
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 lastSeenDate = parseISOString(batteryData.eventUpdatedTimestamp.iso);
const distance = parseInt(batteryData.estimatedDistanceToEmptyKm);
const widget = new ListWidget();
widget.url = "polestar-explore://";
widget.backgroundColor = Color.white();
widget.addSpacer(1);
// Show app icon and title
const titleStack = widget.addStack();
const titleElement = titleStack.addText(" Polestar 2");
titleElement.textColor = Color.black();
//titleElement.textOpacity = 0.7;
titleElement.font = Font.mediumSystemFont(14);
titleStack.addSpacer(16);
const appIconElement = titleStack.addImage(fm.readImage(iconpath));
appIconElement.imageSize = new Size(20, 20);
appIconElement.cornerRadius = 4;
widget.addSpacer(6);
// Car Picture Stack
const contentStack = widget.addStack();
const carImageElement = contentStack.addImage(fm.readImage(picpath));
carImageElement.imageSize = new Size(130, 60);
//contentStack.addSpacer(1);
// Battery Info Stack
const batteryInfoStack = widget.addStack();
//batteryInfoStack.layoutVertically();
batteryInfoStack.centerAlignContent()
//batteryInfoStack.addSpacer(1);
// Battery Percent Value
//const batteryPercent = tibberData.battery.percent;
//const isCharging = tibberData.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(30, 30);
batterySymbolElement.tintColor = getBatteryPercentColor(batteryPercent);
//batteryPercentStack.addSpacer(2);
const batteryPercentText = batteryPercentStack.addText(`${batteryPercent} %`);
//textStack.addText(`${batteryPercent}%`);
batteryPercentText.textColor = getBatteryPercentColor(batteryPercent);
batteryPercentText.font = Font.boldSystemFont(14);
// Distance
//const distanceStack = widget.addStack();
//batteryPercentStack.addSpacer(1);
const distanceStackText = batteryPercentStack.addText(` ${distance} km`);
distanceStackText.textColor = getBatteryPercentColor(batteryPercent);
distanceStackText.font = Font.boldSystemFont(13);
// Footer Stack
const footerStack = widget.addStack();
footerStack.addSpacer(8);
// Add last seen indicator
//const lastSeenDate = new Date(tibberData.lastSeen);
const lastSeenText = lastSeenDate.toLocaleString();
let lastSeenElement;
//if (LAST_SEEN_RELATIVE_DATE) {
//lastSeenElement = footerStack.addDate(lastSeenDate);
//lastSeenElement.applyRelativeStyle();
//} else {
lastSeenElement = footerStack.addText(lastSeenText);
//}
lastSeenElement.textColor = Color.black();
lastSeenElement.font = Font.mediumSystemFont(10);
lastSeenElement.textOpacity = 0.5;
//lastSeenElement.minimumScaleFactor = 0.5;
lastSeenElement.centerAlignText();
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("https://pc-api.polestar.com/eu-north-1/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("https://pc-api.polestar.com/eu-north-1/mystar-v2");
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;
}
function getUrlEncodedParams(object) {
return Object.keys(object)
.map((key) => `${key}=${encodeURIComponent(object[key])}`)
.join("&");
}
/************************
* Load files from iCloud
************************/
async function loadicloud(filename) {
let path = fm.joinPath(fm.documentsDirectory(),filename);
await fm.downloadFileFromiCloud(path);
return path;
}
/*************
* Formatters
*************/
function getBatteryPercentColor(percent) {
if (percent > 60) {
return Color.green();
} else if (percent > 30) {
return Color.orange();
} else {
return Color.red();
}
}
function parseISOString(s) {
var b = s.split(/\D+/);
return new Date(Date.UTC(b[0], --b[1], b[2], b[3], b[4], b[5], b[6]));
}
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}`);
}
Viel Spaß!

