SoC medium homescreen widget [iOS]

dann ist da aber irgendwas falsch gelaufen, bei mir sinds 2% (iOS 16.5 - iPhone 11)

Ok, gebe dem Widget eine zweite Chance, nachdem die Batterie Statistik wieder zurückgesetzt wurde. Vielleicht lag es an meinen „wilden Codeänderungen“ :stuck_out_tongue_winking_eye:

Ok, muss mich korrigieren, doch kein Batterie Problem.
Hab mir das Widget etwas angepasst und ein anderes Bild vom Polestar verwendet, was ich mir dann von meiner iCloud lade.

2 „Gefällt mir“

Schaut mal, API zum Auslesen der Lade- / Verbrauchsdaten - #23 von OliverR, damit müßte es dann vielleicht auch ohne Tibber gehen.

Jop die API kenn ich, hab auch schon ein Draft für v2 der Widgets direkt über die Polestar API statt über Tibber. Also kommt demnächst :wink:

2 „Gefällt mir“

Für das Lockscreen Widget hab ich es schon eingebaut, Medium kommt auch noch, aber braucht noch bisschen.

Hab mal vorauseilend eine neue Version meines kleinen SoC Widgets für den Homescreen gebaut.
Dank neuer Polestar API hat es nun auch die Reichweite auf den Screen geschafft … :blush:
Vielen Dank an @salkin für die ganze Vorarbeit!

IMG_1250

6 „Gefällt mir“

Hallo, das sieht optimal aus!
kannst du dein Script zur Verfügung stellen?

lg Andreas

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ß!


2 „Gefällt mir“

Vielen Dank! Mal sehen ob es nach MFA auch noch geht.
IMG_1955

1 „Gefällt mir“

Hi zusammen,
Neue Version des medium-widget ist jetzt verfügbar.
Basiert auf der richtigen Polestar API und hat ein paar neue Konfigurationsoptionen und Support für Light Mode und unterschiedliche Bilderwinkel :slight_smile:

8 „Gefällt mir“

Sehr schön, vielen Dank!

Richtig gut geworden, vielen Dank!

Klasse, sogar mehr Daten als in der App (Tachostand)
Danke!!

Funktioniert einwandfrei!
Eine Frage hätte ich: wie und wo konfiguriere ich die unterschiedlichen Blickwinkel?

Für IMG_ANGLE einen Wert 0-5 eintragen :slight_smile:

2 „Gefällt mir“

Danke! Jetzt ist es für mich perfekt.

Etwas spät, aber : Hammergeil !!!

Und mit ein bisschen modding geht’s auch. :smiley:
Wobei das Bild vom Polestar jetzt per Hand bearbeitet und dann hardcoded wurde. Da das Original Sooooo viel padding bzw. einen ganz dezenten Schatten hatte.
(Und das, während der Arbeitszeit, Hahaha)

Frage: Wann ist denn das Blitz-Symbol links neben dem SoC rot und wann grün?
Bildschirmfoto 2024-02-05 um 16.28.05

Ich hatte das Auto die Tage in der Garage angesteckt, aber nicht geladen (weil PV-Überschuss nicht reichte). Da war das Symbol rot.

Jetzt ist das Symbol grün. Ich habe heute geladen (von 78-80%) auf Ende Ladeziel.