==== Battery Report (Generate + Parse + Summary + Health Compliance) ====
Immy combined script: $Method in [“Get”,“Test”,“Set”]
No param block. Optional Immy variables:
OutputPath (String) e.g. C:\Windows\Temp\battery-report.html
MaxAgeMinutes (Int) e.g. 1440
Overwrite (Boolean) e.g. True
MinHealthPercent (Int) e.g. 75
$ErrorActionPreference = ‘Stop’
— Defaults if Immy doesn’t provide values —
if (-not $OutputPath -or [string]::IsNullOrWhiteSpace($OutputPath)) { $OutputPath = “C:\Windows\Temp\battery-report.html” }
if (-not $MaxAgeMinutes -or $MaxAgeMinutes -lt 1) { $MaxAgeMinutes = 1440 }
if ($null -eq $Overwrite) { $Overwrite = $true }
if ($null -eq $MinHealthPercent -or $MinHealthPercent -lt 1) { $MinHealthPercent = 75 }
function Ensure-ParentFolder {
param([string]$Path)
$parent = Split-Path -Parent $Path
if (-not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
}
function Get-ReportFileStatus {
if (-not (Test-Path -LiteralPath $OutputPath)) {
return [pscustomobject]@{ OutputPath=$OutputPath; Exists=$false; LastWriteTime=$null; AgeMinutes=$null; LengthBytes=$null }
}
$fi = Get-Item -LiteralPath $OutputPath
$age = [math]::Round(((Get-Date) - $fi.LastWriteTime).TotalMinutes, 2)
return [pscustomobject]@{ OutputPath=$OutputPath; Exists=$true; LastWriteTime=$fi.LastWriteTime; AgeMinutes=$age; LengthBytes=$fi.Length }
}
function New-BatteryReport {
Ensure-ParentFolder -Path $OutputPath
if ((Test-Path -LiteralPath $OutputPath) -and (-not $Overwrite)) { return }
$args = "/c powercfg /batteryreport /output `"$OutputPath`""
$p = Start-Process -FilePath "cmd.exe" -ArgumentList $args -Wait -PassThru -NoNewWindow
if ($p.ExitCode -ne 0) { throw "powercfg exited with code $($p.ExitCode)." }
if (-not (Test-Path -LiteralPath $OutputPath)) { throw "powercfg completed but output file was not created: $OutputPath" }
}
function HtmlDecode([string]$s) { if ($null -eq $s) { return $null }; [System.Net.WebUtility]::HtmlDecode($s) }
function Strip-Tags([string]$s) {
if ($null -eq $s) { return $null }
$t = ($s -replace ‘<br\s*/?>’, “n") -replace '<[^>]+>', '' $t = HtmlDecode $t $t = ($t -replace '\r', '') -replace '[ \t]+', ' ' $t = ($t -replace '\n\s+', "n”).Trim()
return $t
}
function Get-FirstMatch {
param(
[string]$Text,
[string]$Pattern,
[System.Text.RegularExpressions.RegexOptions]$Options = ([System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Singleline)
)
$m = [regex]::Match($Text, $Pattern, $Options)
if ($m.Success) { return $m.Groups[1].Value }
return $null
}
function Extract-SectionTableHtml {
param([string]$Html,[string]$SectionTitle)
$pattern = “(?is)<h[1-4][^>]>\s” + [regex]::Escape($SectionTitle) + “\s*</h[1-4]>\s*(?:.|\n)?(<table\b.?)”
return Get-FirstMatch -Text $Html -Pattern $pattern
}
function Parse-TableToObjects {
param([string]$TableHtml)
if (-not $TableHtml) { return @() }
$rowMatches = [regex]::Matches($TableHtml, '(?is)<tr\b.*?</tr>')
if ($rowMatches.Count -eq 0) { return @() }
# headers (th only)
$headers = $null
foreach ($rm in $rowMatches) {
if ($rm.Value -match '(?is)<th\b') {
$th = [regex]::Matches($rm.Value, '(?is)<th\b[^>]*>(.*?)</th>')
if ($th.Count -gt 0) {
$headers = @()
foreach ($h in $th) { $headers += (Strip-Tags $h.Groups[1].Value) }
break
}
}
}
$objects = @()
foreach ($rm in $rowMatches) {
if ($rm.Value -match '(?is)<th\b') { continue }
$cells = [regex]::Matches($rm.Value, '(?is)<t[dh]\b[^>]*>(.*?)</t[dh]>')
if ($cells.Count -eq 0) { continue }
$vals = @()
foreach ($c in $cells) { $vals += (Strip-Tags $c.Groups[1].Value) }
# skip spacer rows (like <tr style="height:0.4em;"></tr>)
if ($vals.Count -eq 0 -or ($vals -join '').Trim().Length -eq 0) { continue }
$o = [ordered]@{}
if ($headers -and $headers.Count -eq $vals.Count) {
for ($i=0; $i -lt $headers.Count; $i++) { $o[$headers[$i]] = $vals[$i] }
} else {
for ($i=0; $i -lt $vals.Count; $i++) { $o["Col$($i+1)"] = $vals[$i] }
}
$objects += [pscustomobject]$o
}
return $objects
}
function Parse-KeyValueRowsToObject {
param(
[object]$Rows,
[string]$KeyProp = “Col1”,
[string]$ValProp = “Col2”
)
$kv = [ordered]@{}
foreach ($r in $Rows) {
$k = (($r.$KeyProp) + ‘’).Trim()
$v = (($r.$ValProp) + ‘’).Trim()
if ($k) { $kv[$k] = $v }
}
return [pscustomobject]$kv
}
function Extract-TopSystemInfoTableHtml {
param([string]$Html)
# First table after
(your report places COMPUTER NAME/BIOS/OS BUILD there)
return Get-FirstMatch -Text $Html -Pattern ‘(?is)<h1[^>]>.?
\s*(<table\b.*?)’
}
function Extract-InstalledBatteriesHeader {
param([string]$InstalledTableHtml)
# In your file, thead has: BATTERY 1
$hdr = Strip-Tags (Get-FirstMatch -Text $InstalledTableHtml -Pattern ‘(?is)<thead\b.?<tr\b.?<t[dh][^>]>\s</t[dh]>\s*<t[dh][^>]>(.?)</t[dh]>.?.?’)
if ([string]::IsNullOrWhiteSpace($hdr)) { return $null }
return ($hdr -replace ‘\s+’, ’ ').Trim()
}
function Normalize-InstalledBatteries {
param([string]$InstalledTableHtml, [object]$Rows)
if (-not $Rows -or $Rows.Count -eq 0) { return @() }
# Case A: "key/value" battery table (your format): Col1 = NAME, Col2 = value
$looksKeyValue =
($Rows[0].PSObject.Properties.Name -contains 'Col1') -and
($Rows[0].PSObject.Properties.Name -contains 'Col2') -and
(@($Rows | Where-Object { ($_.Col1 + '') -match '(?i)DESIGN CAPACITY|FULL CHARGE CAPACITY|CYCLE COUNT|NAME' }).Count -ge 2)
if ($looksKeyValue) {
$batName = Extract-InstalledBatteriesHeader -InstalledTableHtml $InstalledTableHtml
$batObj = Parse-KeyValueRowsToObject -Rows $Rows -KeyProp "Col1" -ValProp "Col2"
if ($batName) {
# add a friendly battery label
$batObj | Add-Member -NotePropertyName "BATTERY" -NotePropertyValue $batName -Force
}
return @($batObj)
}
# Case B: matrix format (Col1 = key, Col2..ColN = Battery columns)
$first = $Rows[0]
$colNames = @($first.PSObject.Properties.Name)
if ($colNames.Count -ge 3 -and $colNames[0] -match '^Col1$') {
$keyCol = $colNames[0]
$batteryCols = $colNames[1..($colNames.Count-1)]
$batteryObjects = @()
foreach ($bcol in $batteryCols) {
$o = [ordered]@{ "BATTERY" = $bcol }
foreach ($row in $Rows) {
$k = (($row.$keyCol) + '').Trim()
if ([string]::IsNullOrWhiteSpace($k)) { continue }
$v = (($row.$bcol) + '').Trim()
$o[$k] = $v
}
$batteryObjects += [pscustomobject]$o
}
$batteryObjects = $batteryObjects | Where-Object {
($_.PSObject.Properties | Where-Object { $_.Name -ne 'BATTERY' -and -not [string]::IsNullOrWhiteSpace(($_.Value + '')) }).Count -gt 0
}
return @($batteryObjects)
}
# Case C: already one-row-per-battery (rare here, but keep)
return @($Rows)
}
function Parse-CapacityToMWh {
param([string]$Text)
if ([string]::IsNullOrWhiteSpace($Text)) { return $null }
$t = $Text -replace ‘,’, ‘’
$m = [regex]::Match($t, ‘(\d+)\s*mWh’, ‘IgnoreCase’)
if ($m.Success) { return [int64]$m.Groups[1].Value }
$m2 = [regex]::Match($t, ‘(\d+)’, ‘IgnoreCase’)
if ($m2.Success) { return [int64]$m2.Groups[1].Value }
return $null
}
function Get-ValueCI {
param([psobject]$Obj, [string]$Names)
if ($null -eq $Obj) { return $null }
foreach ($p in $Obj.PSObject.Properties) {
foreach ($n in $Names) {
if (($p.Name + ‘’).Trim().ToLower() -eq ($n + ‘’).Trim().ToLower()) {
return ($p.Value + ‘’).Trim()
}
}
}
return $null
}
function Build-Summary {
param([pscustomobject]$Parsed)
$sys = $Parsed.SystemInformation
$bats = $Parsed.InstalledBatteries
$batteryCount = if ($bats) { @($bats).Count } else { 0 }
$firstBat = if ($batteryCount -gt 0) { @($bats)[0] } else { $null }
$designTxt = Get-ValueCI -Obj $firstBat -Names @('DESIGN CAPACITY','Design capacity')
$fullTxt = Get-ValueCI -Obj $firstBat -Names @('FULL CHARGE CAPACITY','Full charge capacity')
$cycleTxt = Get-ValueCI -Obj $firstBat -Names @('CYCLE COUNT','Cycle count')
$design = Parse-CapacityToMWh $designTxt
$full = Parse-CapacityToMWh $fullTxt
$health = $null
if ($design -and $full -and $design -gt 0) { $health = [math]::Round(($full / $design) * 100, 2) }
$computerName = Get-ValueCI -Obj $sys -Names @('COMPUTER NAME','Computer name','Computer Name')
$osBuild = Get-ValueCI -Obj $sys -Names @('OS BUILD','OS build','OS Build')
$bios = Get-ValueCI -Obj $sys -Names @('BIOS','Bios')
$reportGenerated= Get-ValueCI -Obj $sys -Names @('REPORT TIME','Report time','Report Time')
$compliant = $false
if ($null -ne $health) { $compliant = ($health -ge $MinHealthPercent) }
return [pscustomobject]@{
BatteryCount = $batteryCount
DesignCapacity_mWh = $design
FullChargeCapacity_mWh = $full
HealthPercent = $health
CycleCount = $cycleTxt
ComputerName = $computerName
OSBuild = $osBuild
BIOS = $bios
ReportGenerated = $reportGenerated
HealthThresholdPercent = $MinHealthPercent
HealthCompliant = $compliant
}
}
function Parse-BatteryReport {
param([string]$Path)
$html = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
$title = Strip-Tags (Get-FirstMatch $html '(?is)<title[^>]*>(.*?)</title>')
$h1 = Strip-Tags (Get-FirstMatch $html '(?is)<h1[^>]*>(.*?)</h1>')
# System info is the first table after H1 (your file format)
$sysTableHtml = Extract-TopSystemInfoTableHtml -Html $html
$sysRows = Parse-TableToObjects -TableHtml $sysTableHtml
$systemInfo = $null
if ($sysRows -and $sysRows.Count -gt 0) { $systemInfo = Parse-KeyValueRowsToObject -Rows $sysRows -KeyProp "Col1" -ValProp "Col2" }
# Other sections (these DO have h2)
$installedHtml = Extract-SectionTableHtml -Html $html -SectionTitle "Installed batteries"
$installedRows = Parse-TableToObjects -TableHtml $installedHtml
$installed = Normalize-InstalledBatteries -InstalledTableHtml $installedHtml -Rows $installedRows
$recentUsageHtml = Extract-SectionTableHtml -Html $html -SectionTitle "Recent usage"
$batteryUsageHtml = Extract-SectionTableHtml -Html $html -SectionTitle "Battery usage"
$usageHistoryHtml = Extract-SectionTableHtml -Html $html -SectionTitle "Usage history"
$capHistHtml = Extract-SectionTableHtml -Html $html -SectionTitle "Battery capacity history"
$lifeEstHtml = Extract-SectionTableHtml -Html $html -SectionTitle "Battery life estimates"
$parsed = [pscustomobject]@{
Report = [pscustomobject]@{
Title = $title
Header = $h1
OutputPath = $Path
}
SystemInformation = $systemInfo
InstalledBatteries = $installed
RecentUsage = Parse-TableToObjects -TableHtml $recentUsageHtml
BatteryUsage = Parse-TableToObjects -TableHtml $batteryUsageHtml
UsageHistory = Parse-TableToObjects -TableHtml $usageHistoryHtml
BatteryCapacityHistory = Parse-TableToObjects -TableHtml $capHistHtml
BatteryLifeEstimates = Parse-TableToObjects -TableHtml $lifeEstHtml
}
$parsed | Add-Member -MemberType NoteProperty -Name Summary -Value (Build-Summary -Parsed $parsed) -Force
return $parsed
}
function Write-BatterySummary {
param([pscustomobject]$StatusObject)
if (-not $StatusObject -or -not $StatusObject.Parsed -or -not $StatusObject.Parsed.Summary) { return }
$s = $StatusObject.Parsed.Summary
$path = $StatusObject.FileStatus.OutputPath
$healthText = if ($null -ne $s.HealthPercent) { "$($s.HealthPercent)%" } else { "N/A" }
$cycleText = if (-not [string]::IsNullOrWhiteSpace($s.CycleCount)) { $s.CycleCount } else { "N/A" }
$designText = if ($null -ne $s.DesignCapacity_mWh) { "$($s.DesignCapacity_mWh) mWh" } else { "N/A" }
$fullText = if ($null -ne $s.FullChargeCapacity_mWh) { "$($s.FullChargeCapacity_mWh) mWh" } else { "N/A" }
Write-Host ""
Write-Host "==== Battery Summary ===="
Write-Host "Battery Count: $($s.BatteryCount)"
Write-Host "Battery Health: $healthText"
Write-Host "Health Threshold: $($s.HealthThresholdPercent)%"
Write-Host "Health Status: $(if ($s.HealthCompliant) { 'PASS' } else { 'FAIL' })"
Write-Host "Cycle Count: $cycleText"
Write-Host "Design Capacity: $designText"
Write-Host "Full Charge Capacity: $fullText"
Write-Host ""
Write-Host "Computer: $(if ($s.ComputerName) { $s.ComputerName } else { 'N/A' })"
Write-Host "OS Build: $(if ($s.OSBuild) { $s.OSBuild } else { 'N/A' })"
Write-Host "BIOS: $(if ($s.BIOS) { $s.BIOS } else { 'N/A' })"
Write-Host ""
Write-Host "For full details view the report at:"
Write-Host "$path"
Write-Host "========================="
}
function Get-FullStatusObject {
$fileStatus = Get-ReportFileStatus
$parsed = $null
$parseError = $null
if ($fileStatus.Exists) {
try { $parsed = Parse-BatteryReport -Path $OutputPath }
catch { $parseError = $_.Exception.Message }
}
return [pscustomobject]@{
FileStatus = $fileStatus
Parsed = $parsed
ParseError = $parseError
}
}
switch ($Method) {
"Get" {
$result = Get-FullStatusObject
Write-BatterySummary -StatusObject $result
return $result
}
"Test" {
$status = Get-FullStatusObject
if (-not $status.FileStatus.Exists) { return $false }
if ($status.FileStatus.AgeMinutes -gt $MaxAgeMinutes) { return $false }
if ($null -eq $status.Parsed -or $status.ParseError) { return $false }
$health = $status.Parsed.Summary.HealthPercent
if ($null -eq $health) { return $false }
return ($health -ge $MinHealthPercent)
}
"Set" {
New-BatteryReport
$result = Get-FullStatusObject
Write-BatterySummary -StatusObject $result
return $result
}
default {
throw "Unknown Method '$Method'. Expected Get, Test, or Set."
}
}