Dynamische Änderung des Energiesparplans

Als Notebook-Nutzer stehe ich oft vor dem Problem, das mir abhängig vom jeweils eingestellten Energiesparplan, entweder die für komplexe Arbeiten notwendige Power fehlt oder die Batterie ist gerade dann alle, wenn ich sie am notwendigsten brauche. Der von Microsoft vorbesetzte Energiesparplan “Ausbalanciert” stellt eigentlich nur ein Workaround zwischen viel Power und Batteriesparen dar und ist für die meisten Anwendungsfälle ungeeignet.

Da nicht überall ein Stromanschluss zur Verfügung steht und auch moderne Batterien nur ein begrenztes Ladevolumen haben, rätsel ich bereits einige Zeit daran herum, wie ich dynamisch den Energiesparplan meines Notebooks ändern kann, abhängig davon ob er mit dem Stromnetz verbunden ist oder im Batteriebetrieb läuft. Bisher habe ich es so gelöst, dass ich manuell auf Energiesparmodus oder High Performance umgestellt (und dies meistens vergessen) habe.

image

Nachdem ich nun endlich eine Lösung für dieses Problem gefunden haben und in der Vermutung, dass auch andere Leute mit diesem Problem kämpfen, möchte ich Euch an meiner Lösung teilhaben lassen.

Ausgangspunkt für meine Recherche war die Möglichkeit den Energiesparplan skriptgesteuert anzupassen. Als Skriptsprache verwende ich PowerShell (damit kenne ich mich halt einigermaßen aus). Zum Anpassen des Energiesparplans gibt es zwei Varianten:

  1. PowerCfg.exe
  2. Get-WmiObject -Namespace „root\cimv2\power“ -Class Win32_PowerPlan

Da der direkte Zugriff auf die API mehr Möglichkeiten bietet, habe ich mich für die Variante 2 entschieden. Die nächste Herausforderung bestand nun darin, festzustellen, ob das Notebook am Stromkabel hängt oder ob es im Batteriebetrieb ausgeführt wird. Dazu habe ich wiederum ein WMI-Objekt der Klasse Win32_Battery verwendet. Anschließend galt es noch herauszufinden, welcher Energiesparplan aktuell aktiv ist. Auch dafür konnte ich die WMI-Klasse Win32_PowerPlan verwenden. Nachdem ich alle meine gesammelten Informationen zusammengenommen (und noch ein wenig drumrumbaut habe) habe ich folgendes Skript erhalten:

[String]$PowerPlanID = "a1841308-3541-4fab-bc81-f71556f20b4a" # Energiesparmodus            
if ((Get-WmiObject -Class Win32_Battery).BatteryStatus -eq 2) { # Netzwerk            
    $PowerPlanID = "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c" #High Performance            
}            
[String]$CurrentPowerPlan = (Get-WmiObject -Namespace "root\cimv2\power" -Class Win32_PowerPlan -Filter "IsActive ='true'").InstanceID.ToString()            
if (([RegEx]"{(.*?)}$").Match($CurrentPowerPlan).Value -ne "{$PowerPlanID}") {            
"change"            
    $p = Get-WmiObject -Namespace "root\cimv2\power" -Class Win32_PowerPlan | Where-Object {$PSItem.InstanceID -eq "Microsoft:PowerPlan\{$PowerPlanID}"}            
    if ($p) {            
        if ($p.Activate().ReturnValue -eq $true) {            
            return 0            
        }            
    }            
}

Das war nun schon perfekt dazu geeignet den Energiesparplan entsprechend umzuschalten, wenn das Stromkabel eingesteckt bzw. entfernt wird. Doch jedes Mal das Skript aufzurufen, wenn ich das Stromkabel einstecke, ist irgendwie umständlich. Deshalb habe ich nach einer Möglichkeit der weiteren Automatisierung gesucht und bin in der WMI-Klasse Win32_PowerManagementEvent fündig geworden. Diese Klasse kann mir mitteilen, wenn sich an der Stromversorgung etwas ändert. Dafür habe ich einen WMI-Event auf meinem PC registriert, der Änderungen des Status mitteilt und anschließend eine Aktion ausführt.

Damit ich nicht das ganze Skript als Action für den Event einfügen muss, habe ich um das oben dargestellte Skript eine Funktion (CmdLet) gepackt. Führt man nun das PowerShell Skript aus, wird bei jeder Änderung von Netzbetrieb auf Batterie und zurück sofort auf den richtigen Energiesparplan umgeschaltet. Cool!

Register-WmiEvent -Query "Select * from Win32_PowerManagementEvent where EventType = 10" -Action {Set-PowerPlan}

Damit nun auch bei jedem neuen Anmelden am Notebook dieser Event aktiv wird, muss er neu registriert werden. Außerdem sollte das PowerShell-Fenster aktiv/geöffnet bleiben, sonst verschwindet der registrierte Event.

Das kann man wieder manuell tun oder weiter automatisieren. Zur Automatisierung gibt es wieder zwei Möglichkeiten:

  1. Autostart (Erstellen einer CMD-Datei zum Ausführen des PowerShell-Programms) oder AutoRun (Eintrag in der Registry z. B. unter HKCU:\\Software\Microsoft\Windows\CurrentVersion\Run)image New-ItemProperty -Path „HKCU:\\Software\Microsoft\Windows\CurrentVersion\Run“ ` -Name „SetPowerPlan“ ` -PropertyType „String“ ` -Value „C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe -WindowStyle hidden -file c:\temp\Blog_PowerPlan2.ps1“
  2. Windows Task

Die erste Lösung funktionierte prima, doch da ich nur wenig Konfigurationsmöglichkeiten habe, hatte ich mich für die zweite Lösung entschieden. Nachdem ich mit meinen ersten Versuchen kläglich gescheitert bin, bin ich schlussendlich auf eine Lösung gekommen, die tatsächlich funktioniert und die ich nun bereits seit einiger Zeit erfolgreich im Einsatz habe.

[Scriptblock]$code = {            
[ScriptBlock]$SetPowerPlan = {            
    $e = $Event.SourceEventArgs.NewEvent            
            
    [String]$PowerPlanID = "a1841308-3541-4fab-bc81-f71556f20b4a"; # Energiesparmodus            
    if ((Get-WmiObject -Class Win32_Battery).BatteryStatus -eq 2) { # Netzwerk            
        $PowerPlanID = "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c"; #High Performance            
    }            
    [String]$CurrentPowerPlan = (Get-WmiObject -Namespace "root\cimv2\power" -Class Win32_PowerPlan -Filter "IsActive ='true'").InstanceID.ToString();            
    if (([RegEx]"{(.*?)}$").Match($CurrentPowerPlan).Value -ne "{$PowerPlanID}") {            
        $p = Get-WmiObject -Namespace "root\cimv2\power" -Class Win32_PowerPlan | Where-Object {$PSItem.InstanceID -eq "Microsoft:PowerPlan\{$PowerPlanID}"};            
        if ($p) {            
            if ($p.Activate().ReturnValue -eq $true) {            
                Write-Host "Switch to PowerPlan $($p.ElementName)";            
            }            
        }            
    }            
}            
Unregister-Event  -SourceIdentifier "PPlan"  -ErrorAction SilentlyContinue             
            
$wmiEvent = Register-WmiEvent -Query "Select * from Win32_PowerManagementEvent where EventType = 10" -Action $SetPowerPlan -SourceIdentifier "PPlan";            
            
While ($true) {            
    Start-Sleep -Seconds 1            
}            
Unregister-Event  -SourceIdentifier "PPlan"  -ErrorAction SilentlyContinue             
Remove-Event -SourceIdentifier "PPlan"            
}            
            
#& $code            
            
$bytes = [System.Text.Encoding]::Unicode.GetBytes($code)            
$encodedCommand = [Convert]::ToBase64String($bytes)            
# powershell.exe -encodedCommand $encodedCommand            
            
$t = Get-ScheduledTask  -TaskName "Set-PowerPlan" -ErrorAction SilentlyContinue            
if ($t) {            
    Unregister-ScheduledTask -TaskName "Set-PowerPlan" -Confirm:$false            
}            
$Task = New-ScheduledTaskAction -Execute "%Windir%\system32\WindowsPowerShell\v1.0\powershell.exe" -Argument "-WindowStyle Hidden -encodedcommand $encodedCommand"            
$Trigger = New-ScheduledTaskTrigger -AtLogOn            
#$Principal = New-ScheduledTaskPrincipal -UserId "Lokaler Dienst" -LogonTyp ServiceAccount -RunLevel Highest            
$Principal = New-ScheduledTaskPrincipal -UserId "$env:userdomain\$env:username" -LogonTyp Interactive -RunLevel Highest            
$Settings = New-ScheduledTaskSettingsSet -Compatibility Win8 -DisallowDemandStart:$false -AllowStartIfOnBatteries:$true -RunOnlyIfNetworkAvailable:$false -DontStopIfGoingOnBatteries:$true            
            
$t = New-ScheduledTask -Action $Task -Principal $Principal -Trigger $Trigger -Settings $Settings |             
    Register-ScheduledTask -TaskName "Set-PowerPlan" -TaskPath "\$env:userdomain\$env:username"            
            
if ($t) {            
    Start-ScheduledTask -TaskName "Set-PowerPlan" -TaskPath  "\$env:userdomain\$env:username"            
}

Dazu habe ich mehrere kleinere Lösungen benötigt, die ich nachfolgend noch kurz beschreiben möchte:

  • Die in einem vorrangegangenen Beispiel verwendete Funktion (CmdLet) hat sich als unpraktisch erwiesen. Deshalb habe ich sie auf einen Scriptblock umgestellt ($SetPowerplan). Da ich einmal dabei war, habe ich dann auch den ganzen Programmblock als Scriptblock implementiert ($code). Ansonsten hätte ich den ursprünglich geplanten Zeichenkette mit dem PowerShell-Skript kodieren müssen (ersetzen von Anführungszeichen [“] durch [\”] und ersetzen von Dollarzeichen [$] z. B. als Variablenkennzeichen durch [`$])
  • Als nächstes habe ich versucht den vorher definierten Scriptblock direkt an die PowerShell.exe als Command-Parameter zu übergeben. Doch auch das hat nicht funktioniert (irgendwie mochte er den Pipe-Operator nicht [|]. Also habe ich den gesamten Block als Base64 codiert und das Ergebnis an den encodedCommand-Parameter der PowerShell übergeben. Sieht zwar im Endeffekt etwas ungewöhnlich aus, jedoch funktioniert es hervorragend und hat sogar noch einige nützliche Nebeneffekte (z. B. die Umgehung der ExecutionPolicy-Einstellung).
  • Nach dem Registrieren des Events habe ich eine Endlosschleife definiert, die den Event am Leben erhält. Ohne diese Schleife würde dieser Event gelöscht und wäre nicht mehr verfügbar –> Nachteil: Dadurch ist ständig ein PowerShell-Prozess aktiv, der Speicher verbraucht. Durch den integrierten Wartezyklus ist jedoch der CPU-Verbrauch nahezu 0 und auch andere Ressourcen des Notebooks werden nicht belastet. Eine mögliche Lösung wäre hier eine andere Programmiersprache (z. B. C#) oder die Möglichkeit einen Event zu registrieren, der auch nach Beenden des Host-Prozesses weiter aktiv bleibt (werde ich wohl noch ein wenig recherchieren dürfen).
  • Nachdem ich bereits in einem früheren Blogartikel die Verwendung von Windows Task in der PowerShell dokumentiert habe, möchte ich Euch nur noch über abweichende Einstellungen informieren:
    • Den Trigger habe ich auf LogOn eingestellt und löse ihn nicht zeitgesteuert aus. Dadurch wird das PowerShell-Skript bei jedem Logon am Notebook automatisch geladen.
    • An den Settings für die Task habe ich einige zusätzliche Parameter verwendet. Bei einigen davon habe ich feststellen müssen, dass die Parameter nicht das tun, was sie dem Namen nach tun sollten. Doch die im vorangegangenen Beispiel von mir verwendeten Einstellungen funktionieren (auf meinem Notebook).

Sollte das Skript auf Euerm System Probleme bereiten oder Ihr noch Fragen/Optimierungen/Verbesserungsvorschläge dazu haben, dürft Ihr gerne Kommentare hinterlassen oder mich direkt kontaktieren.

Advertisements
Über

Die IT-Welt wird immer komplexer und zwischen den einzelnen Komponenten gibt es immer mehr Abhängigkeiten. Nachdem ich durch meine tägliche Arbeit immer wieder vor der Herausforderung stehe, komplexe Probleme zu lösen, möchte ich diese Seite dafür verwenden, Euch den einen oder anderen Tipp zu geben, wenn Ihr vor ähnlichen Aufgabenstellungen steht.

Veröffentlicht in Allgemein, PowerShell

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: