SoC medium homescreen widget [iOS]

Man kann auch ein Widget von ABRP einfügen, um seinen SoC abzulesen und man erhält zusätzlich noch den Standpunkt seines PS2, falls man den vergessen haben sollte! Der obere gefällt mir definitiv besser, der untere kommt erst dann zum Tragen, wenn ich offensichtlich kognitiv abbaue! Aber dann muss ich auch das Google Maps Widget installieren, damit ich auch weiß, wo ich bin …! :innocent:

:ok_hand:t3: das von ABRP aktualisiert allerdings den SoC nur wenn das Auto an ist also meistens nicht während des Ladevorgangs.

1 „Gefällt mir“

Super, dann lag ich ja instinktiv richtig, das ABPR Widget wieder zu löschen! Der Vorteil ist natürlich klasse …! :+1:

Oh man. Ich hätte das Widget bloß vorher nur einmal antippen brauchen. Haha. Fail me.

Dafür hab ich mir mal, ganz schlecht… ne kleine Variante daraus adaptiert:

1 „Gefällt mir“

Sieht gut aus!
Kannst Du Dein Skript dafür teilen?

1 „Gefällt mir“
// This script was downloaded using ScriptDude.
// Do not remove these lines, if you want to benefit from automatic updates.
// source: https://gist.githubusercontent.com/niklasvieth/159c13dd7ef94bd608358ce964b66c7c/raw/polestar-medium-widget.js; docs: https://github.com/niklasvieth/polestar-ios-medium-widget/blob/main/README.md; hash: -342644904;

// 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 TIBBER_EMAIL = "MyEmail";
const TIBBER_PASSWORD = "MyPW";
const LAST_SEEN_RELATIVE_DATE = true; // false

const TIBBER_BASE_URL = "https://app.tibber.com";
const POLESTAR_ICON = "https://www.polestar.com/w3-assets/coast-228x228.png";

// Check that params are set
if (TIBBER_EMAIL === "EMAIL_ADDRESS") {
  throw new Error("Parameter TIBBER_EMAIL is not configured");
}
if (TIBBER_PASSWORD === "PASSWORD") {
  throw new Error("Parameter TIBBER_PASSWORD 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 tibberData = await fetchTibberData();
const widget = await createPolestarWidget(tibberData);

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(tibberData) {
  //const appIcon = await loadImage(POLESTAR_ICON);
  const title = tibberData.name;
  const widget = new ListWidget();
  widget.url = "polestar-explore://";

  // Add background gradient
  const gradient = new LinearGradient();
  gradient.locations = [0, 1];
  gradient.colors = [new Color("4F2683"), new Color("111111")];
  widget.backgroundGradient = gradient;

  // Show app icon and title
  const titleStack = widget.addStack();
  //const titleElement = widget.addText(title);
  //titleElement.textColor = Color.white();
  //titleElement.textOpacity = 0.7;
  //titleElement.font = Font.mediumSystemFont(8);
  //titleStack.addSpacer();
  //const appIconElement = widget.addImage(appIcon);
  //appIconElement.imageSize = new Size(10, 10);
  //appIconElement.cornerRadius = 4;
  //widget.addSpacer(2);

  // Center Stack
  const contentStack = widget.addStack();
  const carImage = await loadImage(tibberData.imgUrl);
  const carImageElement = widget.addImage(carImage);
  carImageElement.imageSize = new Size(150, 80);
  //carImageElement.cornerRadius = -10;
  contentStack.addSpacer();
  //contentStack.centerAlignContent();

  // Battery Info
  const batteryInfoStack = contentStack.addStack();
  batteryInfoStack.layoutVertically();
  batteryInfoStack.addSpacer();

  // Battery Percent Value
  const batteryPercent = tibberData.battery.percent;
  const isCharging = tibberData.battery.isCharging;
  const batteryPercentStack = widget.addStack();
  batteryPercentStack.addSpacer();
  batteryPercentStack.centerAlignContent();
  const batterySymbol = getBatteryPercentIcon(batteryPercent, isCharging);
  const batterySymbolElement = batteryPercentStack.addImage(
    batterySymbol.image
  );
  batterySymbolElement.imageSize = new Size(60, 60);
  batterySymbolElement.tintColor = getBatteryPercentColor(batteryPercent);
  batteryPercentStack.addSpacer(3);

  const batteryPercentText = batteryPercentStack.addText(`${batteryPercent} %`);
  batteryPercentText.textColor = getBatteryPercentColor(batteryPercent);
  batteryPercentText.font = Font.boldSystemFont(24);

  // Footer
  const footerStack = widget.addStack();
  footerStack.layoutVertically();
  //footerStack.addSpacer(1);

  // 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.white();
  lastSeenElement.font = Font.mediumSystemFont(10);
  lastSeenElement.textOpacity = 0.5;
  lastSeenElement.minimumScaleFactor = 0.5;
  lastSeenElement.rightAlignText();
 
  return widget;
}

/********************
 * Tibber API helpers
 ********************/
async function fetchTibberToken() {
  const tokenUrl = `${TIBBER_BASE_URL}/login.credentials`;
  const body = {
    "@type": "login",
    email: TIBBER_EMAIL,
    password: TIBBER_PASSWORD,
  };
  const req = new Request(tokenUrl);
  req.method = "POST";
  req.body = JSON.stringify(body);
  req.headers = {
    "Content-Type": "application/json",
    charset: "utf-8",
  };
  const response = await req.loadJSON();
  return response.token;
}

async function fetchTibberData() {
  const tibberToken = await fetchTibberToken();
  const url = `${TIBBER_BASE_URL}/v4/gql`;
  const body = {
    query:
      "{me{homes{electricVehicles{lastSeen imgUrl name shortName battery{percent chargeLimit isCharging percentColor}}}}}",
  };
  const req = new Request(url);
  req.method = "POST";
  req.body = JSON.stringify(body);
  req.headers = {
    "Content-Type": "application/json",
    charset: "utf-8",
    Authorization: `Bearer ${tibberToken}`,
  };
  const response = await req.loadJSON();
  return response.data.me.homes[0].electricVehicles[0];
}

async function loadImage(url) {
  const req = new Request(url);
  return req.loadImage();
}

/*************
 * Formatters
 *************/
function getBatteryPercentColor(percent) {
  if (percent > 60) {
    return Color.green();
  } else if (percent > 30) {
    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}`);
}

3 „Gefällt mir“

@TheralSadurns
Vielen lieben Dank!

1 „Gefällt mir“

Hat geklappt, aber letztendlich habe ich das Widget wieder runtergeworfen. Scriptable hatte einen Anteil von 50% an meinem Akkuverbrauch, das war mir dann doch etwas zu viel …

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“