Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Leibinger015/3d981c72b91fb498ec2cf17b12a3b56c to your computer and use it in GitHub Desktop.

Select an option

Save Leibinger015/3d981c72b91fb498ec2cf17b12a3b56c to your computer and use it in GitHub Desktop.
My Battery Dashboard for - almost - all iPhone models
// My Battery Dashboard for - almost - all iPhone models
//
// An English-translated Scriptable-App-Script named "BatteryFitness++" – the battery analytics (HTML/WebView variant).
//
// Input 'Analytics' file passed via Share Sheet as a converted (.txt = suffix) file
//
// Script = www.anb030.de (28092025) Make with love.
//
let text = args.plainTexts && args.plainTexts.length > 0 ? args.plainTexts[0] : null;
if (!text) {
let err = `
<html><body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; padding:20px;">
<h1 style="color:#c0392b">⚠<br>Something went wrong.<br>No "Analytics file" was provided.</h1>
<p>ⓘ<br>Please first open the "Analytics" file from the <strong>Settings</strong> app. There, select the options in order: <strong>Privacy & Security → Analytics & Improvements *¹ → Analytics Data</strong>. Then open the desired "Analytics" file, share it via the Share Sheet (Share menu) and save it to Apple’s "Files" app. <br>
<br><em><strong>*¹ Note:</strong> Please enable the switch <strong>Share iPhone & Watch Analytics</strong> at least 24 hours in advance so that the "Analytics..." file can be generated for extraction.</em><br>
<br>ⓘ <br>Locate the file in the “Files” app and change its file extension (suffix) <strong>*²</strong> by long-pressing → context menu option “Rename” from “.synced” to “.txt”. Then share it again via the Share Sheet (Share menu) with the Scriptable script <strong>*³</strong> “BatteryFitness++”. The Scriptable app should then open automatically and display all relevant battery data.<br><br>
<em><strong>*² Note: </strong>In the "Files" app you must enable a small setting: Tap the three "•••" dots at the top right → Scroll down to "View Options" → Scroll further down to "Show All File Extensions" and enable the switch.</em><br><br>
<em><strong>*³ Note: </strong>In the "Scriptable" app you must enable a small setting: Open the created script card named “BatteryFitness++” again via the three dots "•••" → Tap the menu icon “≒” above the keyboard → Then tap the menu item “Share Sheet Inputs” → Check all boxes to enable proper sharing for the Share menu.</em></p>
</body></html>`;
let wvErr = new WebView();
await wvErr.loadHTML(err);
await wvErr.present();
return;
}
// --- Mapping table (Device identifier -> Marketing name) ----------
const deviceMap = {
"iPhone1,1": "iPhone 1 Gen.",
"iPhone1,2": "iPhone 3G",
"iPhone2,1": "iPhone 3GS",
"iPhone3,1": "iPhone 4 GSM",
"iPhone3,2": "iPhone 4 GSMrevA",
"iPhone3,3": "iPhone 4 CDMA",
"iPhone4,1": "iPhone 4S",
"iPhone5,1": "iPhone 5 GSM",
"iPhone5,2": "iPhone 5 GSM+CDMA",
"iPhone5,3": "iPhone 5c GSM",
"iPhone5,4": "iPhone 5c GSM+CDMA",
"iPhone6,1": "iPhone 5s GSM",
"iPhone6,2": "iPhone 5s GSM+CDMA",
"iPhone7,2": "iPhone 6",
"iPhone7,1": "iPhone 6 Plus",
"iPhone8,1": "iPhone 6s",
"iPhone8,2": "iPhone 6s Plus",
"iPhone8,4": "iPhone SE 1 Gen.",
"iPhone9,1": "iPhone 7 Global",
"iPhone9,2": "iPhone 7 Plus Global",
"iPhone9,3": "iPhone 7 GSM",
"iPhone9,4": "iPhone 7 Plus GSM",
"iPhone10,1": "iPhone 8",
"iPhone10,2": "iPhone 8 Plus",
"iPhone10,3": "iPhone X Global",
"iPhone10,6": "iPhone X GSM",
"iPhone11,2": "iPhone XS",
"iPhone11,4": "iPhone XS Max China",
"iPhone11,6": "iPhone XS Max",
"iPhone11,8": "iPhone XR",
"iPhone12,1": "iPhone 11",
"iPhone12,3": "iPhone 11 Pro",
"iPhone12,5": "iPhone 11 Pro Max",
"iPhone12,8": "iPhone SE 2 Gen.",
"iPhone13,1": "iPhone 12 mini",
"iPhone13,2": "iPhone 12",
"iPhone13,3": "iPhone 12 Pro",
"iPhone13,4": "iPhone 12 Pro Max",
"iPhone14,2": "iPhone 13 Pro",
"iPhone14,3": "iPhone 13 Pro Max",
"iPhone14,4": "iPhone 13 mini",
"iPhone14,5": "iPhone 13",
"iPhone14,6": "iPhone SE 3 Gen.",
"iPhone14,7": "iPhone 14",
"iPhone14,8": "iPhone 14 Plus",
"iPhone15,2": "iPhone 14 Pro",
"iPhone15,3": "iPhone 14 Pro Max",
"iPhone15,4": "iPhone 15",
"iPhone15,5": "iPhone 15 Plus",
"iPhone16,1": "iPhone 15 Pro",
"iPhone16,2": "iPhone 15 Pro Max",
"iPhone17,1": "iPhone 16 Pro",
"iPhone17,2": "iPhone 16 Pro Max",
"iPhone17,3": "iPhone 16",
"iPhone17,4": "iPhone 16 Plus",
"iPhone17,5": "iPhone 16e",
"iPhone18,3": "iPhone 17",
"iPhone18,4": "iPhone 17 Air",
"iPhone18,1": "iPhone 17 Pro",
"iPhone18,2": "iPhone 17 Pro Max"
};
// --- Helper functions --------------------------------------------------
function extractValueNum(key) {
let regex = new RegExp(`"${key}":(\\d+)`);
let match = text.match(regex);
return match ? Number(match[1]) : null;
}
function extractDevice() {
// searches for device":"iPhoneXX,X or iPhoneX,X
let regex = /"device":"(iPhone\d+,\d+)"/;
let match = text.match(regex);
return match ? match[1] : null;
}
function formatTemperatureForDisplay(value) {
if (value === null) return "N/A";
if (value < 100) return value.toFixed(1).replace(".0", ",0");
return (value / 10).toFixed(1).replace(".", ",");
}
function healthColor(pct) {
if (pct === null) return "#999";
if (pct >= 90) return "#27ae60"; // green
if (pct >= 85) return "#f1c40f"; // yellow
if (pct >= 80) return "#f39c12"; // orange
return "#c0392b"; // red
}
function formatMonthYear(d) {
let m = (d.getMonth() + 1).toString().padStart(2, "0");
let y = d.getFullYear();
return `${m}.${y}`;
}
function safe(val, fallback = "N/A") {
return (val === null || val === undefined) ? fallback : String(val);
}
function escapeHtml(s) {
if (s === null || s === undefined) return "";
return String(s).replace(/[&<>"']/g, function (m) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[m];
});
}
// --- Extract values ---------------------------------------------------
let deviceCode = extractDevice();
let deviceName = deviceCode && deviceMap[deviceCode] ? deviceMap[deviceCode] : "";
let cycleTotal = extractValueNum("last_value_CycleCount");
let cycleSinceQmax = extractValueNum("last_value_CycleCountLastQmax");
let healthPercent = extractValueNum("last_value_MaximumCapacityPercent");
let currentCapacity = extractValueNum("last_value_AppleRawMaxCapacity");
let designCapacity = extractValueNum("last_value_MaximumFCC");
let totalOperatingTime = extractValueNum("last_value_TotalOperatingTime");
let updateTime = extractValueNum("last_value_UpdateTime");
let minTempRaw = extractValueNum("last_value_MinimumTemperature");
let avgTempRaw = extractValueNum("last_value_AverageTemperature");
let maxTempRaw = extractValueNum("last_value_MaximumTemperature");
// --- Calculations -----------------------------------------------------
let updateDate = updateTime ? new Date(updateTime * 1000) : new Date();
let updateDateStr = updateDate.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' });
let totalDays = totalOperatingTime ? Math.round(totalOperatingTime / 24) : null;
const HOURS_PER_YEAR = 365.25 * 24;
let years = totalOperatingTime ? (totalOperatingTime / HOURS_PER_YEAR) : null;
let yearsStr = years !== null ? years.toFixed(1).replace(".", ",") : "N/A";
let firstUseStr = "unknown";
if (totalOperatingTime !== null && updateTime !== null) {
let firstUseMillis = (updateTime * 1000) - (totalOperatingTime * 3600 * 1000);
let firstUseDate = new Date(firstUseMillis);
firstUseStr = formatMonthYear(firstUseDate);
}
let minTemp = formatTemperatureForDisplay(minTempRaw);
let avgTemp = formatTemperatureForDisplay(avgTempRaw);
let maxTemp = formatTemperatureForDisplay(maxTempRaw);
let currentCapacityStr = currentCapacity !== null ? `${currentCapacity} mAh` : "N/A";
let designCapacityStr = designCapacity !== null ? `${designCapacity} mAh` : "N/A";
let healthPercentStr = healthPercent !== null ? `${healthPercent} %` : "N/A";
let healthBarColor = healthColor(healthPercent);
let healthBarWidth = healthPercent !== null ? Math.max(0, Math.min(100, healthPercent)) : 0;
// --- HTML output ------------------------------------------------------
let html = `
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body { background:#0f1720; color:#e6eef6; margin:0; padding:20px; font-family:-apple-system, BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial; }
.container { max-width:820px; margin:0 auto; }
.card { background:rgba(255,255,255,0.02); border-radius:12px; padding:18px; }
.title { display:flex; align-items:center; gap:12px; }
.logo { width:48px; height:48px; border-radius:10px; background:#111827; display:flex; align-items:center; justify-content:center; font-weight:700; color:#9fb7ff; font-size:18px; }
h1 { margin:0; font-size:18px; }
p.sub { margin:4px 0 12px; color:#9aa4b2; font-size:13px; }
.row { display:flex; gap:16px; flex-wrap:wrap; }
.block { background:rgba(255,255,255,0.03); padding:12px; border-radius:8px; flex:1 1 240px; min-width:200px; }
.label { color:#9aa4b2; font-size:12px; margin-bottom:6px; }
.value { font-size:20px; font-weight:600; color:#eaf2ff; }
.small { font-size:16px; font-weight:600; margin-top:6px; color:#eaf2ff; }
.deviceLabel { color:#9aa4b2; font-size:12px; margin-bottom:4px; }
.deviceName { font-size:22px; font-weight:700; color:#ffffff; margin-bottom:8px; }
.barOuter { background:rgba(255,255,255,0.06); border-radius:999px; height:12px; width:100%; }
.barInner { height:12px; border-radius:999px; width:${healthBarWidth}%; background:${healthBarColor}; }
table { width:100%; margin-top:8px; border-collapse:collapse; }
td { padding:8px 6px; }
.temp { font-weight:700; font-size:16px; }
.muted { color:#9aa4b2; }
.footer { margin-top:14px; color:#9aa4b2; font-size:13px; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="title">
<div class="logo">⚡️</div>
<div>
<h1>Battery Analysis<br>Status from file: ${escapeHtml(updateDateStr)}</h1>
<div class="sub">(Summary)</div>
</div>
</div>
<div class="row" style="margin-top:14px;">
<div class="block">
<div class="deviceLabel">Device Model</div>
<div class="deviceName">${escapeHtml(deviceName)}</div>
<div class="label">Charge Cycles (total)</div>
<div class="value">${escapeHtml(safe(cycleTotal, "N/A"))}</div>
<div class="label" style="margin-top:8px;">Charge Cycles since Calibration</div>
<div class="value">${escapeHtml(safe(cycleSinceQmax, "N/A"))}</div>
</div>
<div class="block">
<div class="label">Battery Health <br>(≥ 90% 🟩, 85–89% 🟨, 80–84% 🟧, ≤ 79% 🟥)</div>
<div class="value">${escapeHtml(healthPercentStr)}</div>
<div class="barOuter"><div class="barInner"></div></div>
<div class="small" style="font-size:13px; font-weight:400;">Battery Capacity according to manufacturer: <br>• Design: ${escapeHtml(designCapacityStr)} <br>• Current: ${escapeHtml(currentCapacityStr)}</div>
</div>
<div class="block">
<div class="label">Operating Time since First Use (Battery Age)</div>
<div class="value">${totalDays !== null ? totalDays + " days" : "N/A"}</div>
<div class="small" style="font-size:13px; font-weight:400;">Approximate first use: ${yearsStr !== "N/A" ? yearsStr + " years ago" : "N/A"} (${escapeHtml(firstUseStr)})</div>
</div>
</div>
<div style="margin-top:14px;">
<div class="label">Temperature History since First Use</div>
<table>
<tr><td class="muted">Lowest Recorded</td><td class="temp">${escapeHtml(minTemp)} °C</td></tr>
<tr><td class="muted">Average</td><td class="temp">${escapeHtml(avgTemp)} °C</td></tr>
<tr><td class="muted">Highest Recorded</td><td class="temp">${escapeHtml(maxTemp)} °C</td></tr>
</table>
</div>
<div class="footer">
UpdateTime: ${escapeHtml(updateDateStr)} • Source: Analytics Data <br>© by www.anb030.de (v20250928)<br> Coded with love ❤️
</div>
</div>
</div>
</body>
</html>
`;
let wv = new WebView();
await wv.loadHTML(html);
await wv.present();
@Leibinger015

Leibinger015 commented Oct 2, 2025 •

Copy link
Copy Markdown
Author

The battery fitness workaround: How Scriptable reveals the actual battery health of your (old) iPhone. ☑︎ With my script and instructions, you can read the battery data directly on your iPhone without external devices such as a Mac.

Instructions and article (in German): anb030.de

IMG_6478

Briefly explained:

ⓘ
Please first open the "Analytics" file from the Settings app. There, select the options in order: Privacy & Security → Analytics & Improvements *¹ → Analytics Data. Then open the desired "Analytics" file, share it via the Share Sheet (Share menu) and save it to Apple’s "Files" app.

*¹ Note: Please enable the switch Share iPhone & Watch Analytics at least 24 hours in advance so that the "Analytics..." file can be generated for extraction.

ⓘ
Locate the file in the “Files” app and change its file extension (suffix) *² by long-pressing → context menu option “Rename” from “.synced” to “.txt”. Then share it again via the Share Sheet (Share menu) with the Scriptable script *³ “BatteryFitness++”. The Scriptable app should then open automatically and display all relevant battery data.

*² Note: In the "Files" app you must enable a small setting: Tap the three "•••" dots at the top right → Scroll down to "View Options" → Scroll further down to "Show All File Extensions" and enable the switch.

*³ Note: In the "Scriptable" app you must enable a small setting: Open the created script card named “BatteryFitness++” again via the three dots "•••" → Tap the menu icon “≒” above the keyboard → Then tap the menu item “Share Sheet Inputs” → Check all boxes to enable proper sharing for the Share menu.

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