Mittwoch, 20. Februar 2019

FHEM HTTP Client

Der eingebaute Client Modus in der fhem.pl funktioniert nur über die Telnet Schnittstelle von FHEM, diese wird aber per default nicht gar nicht mehr definiert.
Ich habe mal in 3 Varianten einen FHEM Client gebaut: als Bash-, Powershell- und Perlscript.

Der FHEM Client verfügt einheitlich über folgende Möglichkeiten:
  • Angabe komplette URL oder nur Portnummer (lokaler Zugriff) [http://<Username>:<Password>@<hostName>:]<portNummer>
    • Zugriff über Standard WEB sofort nach FHEM Installation möglich, csrf Token wird verwendet.
    • Angabe von Basic Auth in der URL möglich.
  • FHEM Befehle als:
    • Argument(e) (analog fhem.pl)
    • Dateiname der Befehlsdatei mit Codeschnipseln (z.B. ganz oder teilweise fhem.cfg).
    • Zeilen über die Pipeline (cat Befehlsdatei | fhemcl 8083)
    • Mehrzeilige Definitionen (Zeilenende mit "\")werden verarbeitet.
  • Ausgabe der FHEM Antwort, z.B. bei list Befehlen, analog zum Client Modus in fhem.pl
  • Kurze Hinweise zur Verwendung
Getestet habe ich die Bash und Perl Scripts unter Raspbian, das Powershell Script unter Windows 10. Mit Sicherheit ist es noch nicht völlig frei von bugs! Verwendung also auf eigene Gefahr!

Verwendung

Ich habe alle Scripte auf GitHub abgelegt und halte sie dort auch aktuell.
Am einfachsten kann man sich die Scripts per Download auf das System holen, das geht ziemlich einheitlich (auch unter Windows/Powershell) mit wget:
wget -OutFile fhemcl.ps1 https://raw.githubusercontent.com/heinz-otto/fhemcl/master/fhemcl.ps1
wget -O fhemcl.pl https://raw.githubusercontent.com/heinz-otto/fhemcl/master/fhemcl.pl
wget -O fhemcl.sh https://raw.githubusercontent.com/heinz-otto/fhemcl/master/fhemcl.sh
Anmerkung. -OutFile anstatt -O braucht man auf älteren Windows Systemen.

Allgemeiner Ablauf

  1. Es wird zunächst das erste Argument getestet, dies muss vorhanden sein. Es erfolgt ein Test ob nur die Portnummer angegeben wurde, diese wird um localhost erweitert ansonsten wird die url ohne weitere Prüfung übernommen. 
  2. Bei Powershell wird aus username:password noch ein extra "Credential" String gemacht, da Invoke-Webrequest die url nicht einfach so verarbeitet wie curl.
  3. Der csrfToken wird extrahiert und gespeichert.
  4. Dann wird ein Array mit den FHEM Befehlen gebildet, dazu wird 
    • die Pipeline getestet und gelesen
    • das zweite Argument gelesen und auf Dateiname getestet
    • entweder die Datei oder weitere Argumente eingelesen 
  5. Zum Schluss wird eine Schleife über das Befehlsarray abgearbeitet, die Befehle url-encoded  und an den FHEM Server übergeben und die Antwort ausgewertet.
  6. Wieder was dazu gelernt: Wenn man an den HTTP Aufruf &XHR=1 anhängt wird nur Text und kein HTML ausgeliefert. Von der FHEM Antwort wird der Inhalt zwischen <div id='content' > und </div> ausgefiltert und von überflüssigen <pre>/</pre> Zeilen und HTML Tags befreit. Die Ausgabe entspricht der des fhem.pl Clients bzw. der sichtbaren Ausgabe im Browser Fenster.

Die Scripts

Der folgende Code ist Stand der Veröffentlichung. Bitte unbedingt nach aktuellem Code im GitHub schauen!
Bash Variante
#!/bin/bash
# Heinz-Otto Klas 2019
# send commands to FHEM over HTTP
# if no Argument, show usage

if [ $# -eq 0 ]
then
     echo 'fhemcl Usage'
     echo 'fhemcl [http://<hostName>:]<portNummer> "FHEM command1" "FHEM command2"'
     echo 'fhemcl [http://<hostName>:]<portNummer> filename'
     echo 'echo -e "set Aktor01 toggle" | fhemcl [http://<hostName>:]<portNumber>'
     exit 1
fi

# split the first Argument
IFS=:
arr=($1)

# if only one then use as portNumber
# or use it as url
IFS=
if [ ${#arr[@]} -eq 1 ]
then
    if [[ `echo "$1" | grep -E ^[[:digit:]]+$` ]]
    then
        hosturl=http://localhost:$1
    else
        echo "$1 is not a Portnumber"
        exit 1
    fi
else
    hosturl=$1
fi

# get Token 
token=$(curl -s -D - "$hosturl/fhem?XHR=1" | awk '/X-FHEM-csrfToken/{print $2}')

# reading FHEM command, from Pipe, File or Arguments 
# Check to see if a pipe exists on stdin.
cmdarray=()
if [ -p /dev/stdin ]; then
        echo "Data was piped to this script!"
        # If we want to read the input line by line
        while IFS= read -r line; do
              cmdarray+=("${line}")
        done
else
        # Checking the 2 parameter: filename exist or simple commands
        if [ -f "$2" ]; then
            echo "Reading File: ${2}"
            readarray -t cmdarray < ${2}
        else
        echo "Reading further parameters"
        for ((a=2; a<=${#}; a++)); do
            echo "command specified: ${!a}"
            cmdarray+=("${!a}")
        done
        fi
fi

# loop over all lines stepping up. For stepping down (i=${#cmdarray[*]}; i>0; i--)
for ((i=0; i<${#cmdarray[*]}; i++));do 
    # concat def lines with ending \ to the next line
    cmd=${cmdarray[i]}
    while [ ${cmd:${#cmd}-2:1} = '\' ];do 
          ((i++))
          cmd=${cmd::-2}$'\n'${cmdarray[i]}
    done
    echo "proceeding Line $i : "${cmd}
    # urlencode loop over String
    cmdu=''
    for ((pos=0;pos<${#cmd};pos++)); do
        c=${cmd:$pos:1}
        [[ "$c" =~ [a-zA-Z0-9\.\~\_\-] ]] || printf -v c '%%%02X' "'$c"
        cmdu+="$c"
    done
    cmd=$cmdu
    # send command to FHEM and filter the output (tested with list...).
    # give only lines between, including the two Tags back, then remove all HTML Tags 
    curl -s --data "fwcsrf=$token" $hosturl/fhem?cmd=$cmd | sed -n '/<pre>/,/<\/pre>/p' |sed 's/<[^>]*>//g'
done

Perl Variante
#!/usr/bin/env perl
# Heinz-Otto Klas 2019
# send commands to FHEM over HTTP
# if no Argument, show usage

use strict;
use warnings;
use URI::Escape;
use LWP::UserAgent;

my $token;
my $hosturl;
my $fhemcmd;

 if ( not @ARGV ) {
     print 'fhemcl Usage',"\n";
     print 'fhemcl [http://<hostName>:]<portNummer> "FHEM command1" "FHEM command2"',"\n";
     print 'fhemcl [http://<hostName>:]<portNummer> filename',"\n";
     print 'echo -e "set Aktor01 toggle" | fhemcl [http://<hostName>:]<portNumber>',"\n";
     exit;
 }

if ($ARGV[0] !~ m/:/) {
   if ($ARGV[0] eq ($ARGV[0]+0)) { # isnumber?
       $hosturl = "http://localhost:$ARGV[0]";
   }
   else {
       print "$ARGV[0] is not a Portnumber";
       exit(1);
   }
}
else {
    $hosturl = $ARGV[0];
}

# get token 
my $ua = new LWP::UserAgent;
my $url = "$hosturl/fhem?XHR=1/";
my $resp = $ua->get($url);
   $token = $resp->header('X-FHEM-CsrfToken');

my @cmdarray ;

# test the pipe and read 
if (-p STDIN) {
   while(<STDIN>) {
       chomp($_);
       push(@cmdarray,$_);
   }
}
# second Argument is file or command?
if ($ARGV[1] and -e $ARGV[1]) {
    open(DATA, '<', $ARGV[1]);
    while(<DATA>) {
       s/\r[\n]*/\n/gm;      #remove any \r 
       chomp($_);
       push(@cmdarray,$_);
    }
    close(DATA);
}
else {
    for(my $i=1; $i < int(@ARGV); $i++) {
    push(@cmdarray, $ARGV[$i]);
    }
}
#execute commands and print response from FHEMWEB 
for(my $i = 0; $i < @cmdarray; $i++) {
    # concat def lines with ending \ to the next line
    my $cmd = $cmdarray[$i];
    while ($cmd =~ m/\\$/) {
        $i++;
        $cmd = substr($cmd,0, -1)."\n".$cmdarray[$i];
    };
    # url encode the cmd
    $fhemcmd = uri_escape($cmd);
    print "proceeding line $i : $fhemcmd\n";
    $url = "$hosturl/fhem?cmd=$fhemcmd&fwcsrf=$token";
    $resp = $ua->get($url)->content;
    # only between the lines <pre></pre> and remove any HTML Tag
    #funktioniert noch nicht sauber bei massenimport
    my @resparray = split("\n", $resp);
    foreach my $zeile(@resparray){
        if ($zeile !~ /<[^>]*>/ or $zeile =~ /pre>/ or $zeile =~ /NAME/) {
           $zeile =~ s/<[^>]*>//g;
           print "$zeile\n" ;
        }
    }
}

Powershell Variante
<#
.SYNOPSIS
    This Script is a FHEM Client for HTTP
.DESCRIPTION
    FHEM commands could given over the Pipe, Arguments or File.
.EXAMPLE
    fhemcl [http://<hostName>:]<portNummer> "FHEM command1" "FHEM command2"
    fhemcl [http://<hostName>:]<portNummer> filename
    echo "FHEM command"|fhemcl [http://<hostName>:]<portNummer>
.NOTES
    put every FHEM command line in ""
#>
#region Params
param(
    [Parameter(Mandatory=$true,Position=0,HelpMessage="-first 'Portnumber or URL'")]
    [String]$first,
    [Parameter(ValueFromPipeline=$true,ValueFromRemainingArguments=$true)]
    [String[]]$sec
)
#endregion 

# if only one element the use as portNumber
# or use as hosturl
$arr = $first -split ':'
if ($arr.Length -eq 1){
   if ($first -match '^\d+$') {$hosturl="http://localhost:$first"}
       else {
           write-output "is not a Portnumber"
           exit
            }
 } else {$hosturl=$first}
# url contains usernam@password?
if ($arr.Length -eq 4){
     $username = $($arr[1] -split'//')[1]
     $password = $($arr[2] -split '@')[0]
     $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
     $headers = @{
     Authorization=("Basic {0}" -f $base64AuthInfo)
     }

     # cut the account from hosturl 
     $hosturl=$arr[0] + "://"+$($arr[2] -split '@')[1] +":" + $arr[3]
}
# get Token
$token = Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri "$hosturl/fhem?XHR=1" | %{$_.Headers["X-FHEM-csrfToken"]}

# reading commands from Pipe, File or Arguments 
# clear cmdarray and save the Pipeline,
# $input contains all lines from pipeline, $sec contains the last line
$cmdarray=@()
foreach ($cmd2 in $input){$cmdarray += $cmd2}
if ($cmdarray.length -eq 0) {
     if((Test-Path $sec) -And ($sec.Length -eq 1)) {$cmdarray = Get-Content $sec} 
     else {foreach ($cmd2 in $sec){$cmdarray += $cmd2}}
}
# send all commands to FHEM
# there is still an error message with Basic Auth and commands like set Aktor01 ..  e.g. list is without any error.

for ($i=0; $i -lt $cmdarray.Length; $i++) {
   # concat def lines with ending \ to the next line
   $cmd = $cmdarray[$i]
   while($cmd.EndsWith('\')) {$cmd=$cmd.Remove($cmd.Length - 1,1) + "`n" + $cmdarray[$i+1];$i++}
   write-output "proceeding line $($i+1) : $cmd"
   # url encode
   $cmd=[System.Uri]::EscapeDataString($cmd)
   $web = Invoke-WebRequest -Uri "$hosturl/fhem?cmd=$cmd&fwcsrf=$token" -Headers $headers
   if ($web.content.IndexOf("<pre>") -ne -1) {$web.content.Substring($web.content.IndexOf("<pre>"),$web.content.IndexOf("</pre>")-$web.content.IndexOf("<pre>")) -replace '<[^>]+>',''}
}

Benchmark/Leistung

Ich habe mal Schleifen mit 10 gleichen Befehlen (set Aktor01 toggle) abgesetzt um zu sehen ob es große Unterschiede bei den realisierten Scripts gibt.

time for ((i=0; i<10; i++));do perl fhemcl.pl http://raspib:8083 "set Aktor01 toggle";done
time for ((i=0; i<10; i++));do bash fhemcl.sh http://raspib:8083 "set Aktor01 toggle";done
Measure-Command {for ($i=1; $i -le 10; $i++) {.\fhemcl.ps1 http://raspib:8083 "set Aktor01 toggle"}}
Die Bash und Powershell Variante läuft etwa gleich schnell ab und braucht für die 10 Durchläufe etwas über 3 sec. Die Perl Variante benötigt je nach Platform (Pi1/Pi2/Pi3) 2 bis 10 mal solange.

Die Telnet Schnittstelle ist wesentlich schneller! Nimmt man die obige Schleife und schickt sie remote an einen Pi3, dann braucht diese Schleife noch 0.5 Sekunden. Lokal auf einem PiB ausgeführt, sind die Unterschiede wieder minimal.
time for ((i=0; i<10; i++));do echo "set Aktor01 toggle"|nc raspib3plus 7072;done
Telnet ist schnell definiert, und wenn man es nicht mehr braucht wieder gelöscht.
define telnetPort telnet 7072 global

Mittwoch, 30. Januar 2019

VHD - was geht, was nicht?

VHD erzeugen und verwenden

VHDs sind Container Dateien, die ein Festplattenimage enthalten. Ich setze sie der neben Virtualisierung gerne ein, um alte Datenpartitionen zu erhalten oder separate Festplatten zu simulieren. Um sie schnell mal zu erzeugen und auch für "normale" Benutzer bereitzustellen, gibt es verschiedenen Möglichkeiten.

In jedem aktuellen Windows System vorhanden:
Explorer: Man kann VHD Dateien mounten/dismounten - einfach per Doppelklick oder rechter Maustaste. Das funktioniert auch mit normalen Benutzerrechten.
Alle anderen Möglichkeiten brauchen Adminrechte.
diskpart/Datenträgerverwaltung: Mit beiden Tools kann man VHD Dateien erzeugen und mounten/dismounten.
Die Powershell kennt zwei Sets an Cmdlets.
(Dismount, Get, Mount)-DiskImage
Diese Cmdlets sind wahrscheinlich in jeder Windows Version vorhanden. Sie funktionieren mit VHD und ISO Dateien. Für die Verwendung von VHD Dateien braucht man Administrator Rechte.
*-VHD, *-VHDSet, *-VHDSnapshot
Ein neues, umfangreiches Set an Cmdlets welches erst mit der Hyper-V Rolle installiert wird. Mit denen kann man VHDs auch erstellen und umfangreich manipulieren. Doku.

Automatische Bereitstellung.

Zur Laufzeit kann man VHD Dateien leicht interaktiv einbinden und auswerfen, aber kann man VHD Dateien als HDD Ersatz auch beim Systemstart bereitstellen?
Das geht per Taskplaner und diskpart Script.

Praktisches Beispiel

Schritt für Schritt entsteht so eine dynamische VHD Datei mit 10 GB und einer Partition.
Achtung: Die Powershellversion funktioniert nur auf einem Hyper-V "aktiviertem" System:
$VDisk = "D:\VHD\TestPS.vhdx"
New-VHD -Path $VDisk -Dynamic -SizeBytes 10GB
$Disk = Mount-VHD -Path $VDisk -Passthru|Get-Disk
$Disk|Initialize-Disk 
$Disk|New-Partition -UseMaximumSize -AssignDriveLetter|Format-Volume -NewFileSystemLabel "Meine VHD"
Dismount-VHD -Path $VDisk
Schritt für Schritt in diskpart
create vdisk file=D:\VHD\TestDP.vhdx type=expandable maximum=10240
select vdisk file=D:\VHD\TestDP.vhdx 
attach vdisk
create partition primary
format FS=NTFS LABEL="MEINE VHD" QUICK
assign
detach vdisk
Alle diskpart Befehle kann man in eine Textdatei packen, und diese dann als Parameter übergeben.
diskpart /s Scriptdatei
Für die automatische Bereitstellung braucht man nun bloß noch ein paar Befehle. Für Powershell ist das Script ein Einzeiler
Mount-VHD -Path "D:\VHD\TestPS.vhdx"
# Oder so
Mount-DiskImage "D:\VHD\TestPS.vhdx"
Um es mit diskpart automatisch zu erledigen, braucht man wieder ein kurzes Script. Dieses startet man dann wie oben.
select vdisk file="D:\VHD\TestDP.vhdx"
attach vdisk
exit

Das Vorgehen bis hierher hat eventuell ein Problem: Die Zuweisung eines Laufwerkbuchstabens. Das passiert im Zweifelsfall einfach nicht. Wenn dem System die VHD nicht bekannt ist, wird sie eventuell ohne Laufwerksbuchstaben bereitgestellt. Bei dem Diskpart Script kann man das einbauen, der Mount-VHD Befehl kennt nur die Option -NoDriveLetter, bei Mount-DiskImage hat man gar keine Option.

Im Taskplaner (Aufgabenplanung) kann man eine Task für den Systemstart einrichten, damit steht die VHD nach dem Start für alle Benutzer zur Verfügung. So geht es:
Registerkarte Allgemein
Wichtig: Benutzer SYSTEM verwenden und den "Mit höchsten Privilegien" Haken setzen.
Registerkarte Trigger
"beim Start" auswählen.
Registerkarte Aktion
Hier in zwei getrennten Boxen den Programmnamen und die Argumente eintragen:
powershell
# entweder als Befehlsblock
-Command Mount-DiskImage "D:\VHD\TestPS.vhdx"
# alternativ als Scriptfile
-ExecutionPolicy Bypass -File "C:\Tools\Scripts\mountvhd.ps1"

diskpart
/s "C:\Tools\Scripts\mountvhd.txt"
Mit "Ausführen" sollte man die Aufgabe nach dem Erstellen direkt testen!

Um den Laufwerksbuchstaben der gemounteten VHD Datei zu ermitteln, muss man eine Kette von Cmdlets bemühen (Doku):
(Get-DiskImage -ImagePath $VDisk |Get-Disk|Get-Partition|Get-Volume).DriveLetter

Montag, 7. Januar 2019

Kalender in FHEM - auf bestimmte Termine reagieren

Die Grundlage schaffen - ein Calendar Device

Für dieses Beispiel: ein Google Kalender. Als Grundlage brauchen wir die "Privatadresse" im iCal Format (Google Kalender Einstellungen/<Kalendername>/Kalendereinstellungen).
Diese URL wird einfach in die Calendar Definition eingesetzt, das Aktualisierungsintervall setze ich auf einen Tag. Man kann jederzeit ein reload des Kalenders durchführen. Man muss bedenken, dass vor allem viele Serientermine, eine nicht unerheblich Zeit beim reload beanspruchen. Das kann schnell mal mehrere Minuten dauern. In der Grundeinstellung erfolgt das blockierend. Je nach Kalender, kann also nach dem define die Oberfläche für ein paar Minuten "stehen"!
define TestKalender Calendar ical url https://calendar.google.com/calendar/ical/xxx/basic.ics 86400
Um zu testen, ob der Kalender richtig gelesen wird, kann man ihn jetzt einfach abfragen. Eine direkte Anzeige der Termine in Readings erfolgt nämlich nicht.

Abfragen machen

Die Abfrage der Termine ist nicht offensichtlich, es gibt aber einige Beispiele in der englischen Doku. Als Ausgangspunkt für mein Beispiel nehme ich mal die Weboberfläche. In der zweiten Auswahlbox in der get Zeile wählt man einfach events aus.
Als Ergebnis bekommt man in einer Box eine Liste der Kalendereinträge (events), ich habe heute (6.1.) drei Einträge drin. Wie man sieht, auch vergangene.
04.01.2019 08:00 4h Sprechstunde
06.01.2019 08:00 14h Heute frei
07.01.2019 08:00 11h Sprechstunde
In der leeren Box kann man weiter format und filter Angaben machen.

Das Ziel in diesem Beispiel soll sein, auf einen ganz bestimmten Eintrag im Kalender zur Startzeit des Termins etwas auszulösen: z.B. den Server an den Tagen wo Sprechstunde ist, kurz vor Arbeitsbeginn zu starten.

Event auswählen

Dazu erzeugt man am Besten einen Termin in naher Zukunft und schaut sich mit dem Eventmonitor die Events an:
2019-01-06 20:00:00 Calendar TestKalender changed: 79vs3fq7siulo1hskdn4gtht7kgooglecom start
2019-01-06 20:00:00 Calendar TestKalender start: 79vs3fq7siulo1hskdn4gtht7kgooglecom 
2019-01-06 20:00:00 Calendar TestKalender modeUpcoming: 7fcbh4r7snu7iqovask7l8oq9qgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeAlarmOrStart: 44318rlssm81janveuga0olanpgooglecom;79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeChanged: 79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeStart: 44318rlssm81janveuga0olanpgooglecom;79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeStarted: 79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender triggered
2019-01-06 20:00:00 Calendar TestKalender nextWakeup: 2019-01-06 20:05:00
2019-01-06 20:05:00 Calendar TestKalender changed: 79vs3fq7siulo1hskdn4gtht7kgooglecom end
2019-01-06 20:05:00 Calendar TestKalender end: 79vs3fq7siulo1hskdn4gtht7kgooglecom 
2019-01-06 20:05:00 Calendar TestKalender modeAlarmOrStart: 44318rlssm81janveuga0olanpgooglecom
2019-01-06 20:05:00 Calendar TestKalender modeStart: 44318rlssm81janveuga0olanpgooglecom
2019-01-06 20:05:00 Calendar TestKalender modeStarted: 
2019-01-06 20:05:00 Calendar TestKalender modeEnd: 26aq76ljdgjfmo444466faml09googlecom;79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:05:00 Calendar TestKalender modeEnded: 79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:05:00 Calendar TestKalender triggered
2019-01-06 20:05:00 Calendar TestKalender nextWakeup: 2019-01-06 22:00:00
Man sieht jeweils 9 Events beim Start des Termines und 9 Events beim Ende. Allerdings keine Information über den "Lesbaren" Inhalt des Termines, diese kann man mittels der uid ($EVTPART1) auslesen. Es gibt zwei Events, jeweils den ersten und zweiten, die für einen trigger interessant sind.
TestKalender:changed:.*
TestKalender:changed:.*start
TestKalender:start:.*
Beim ersten regExp müsste/könnte man mit $EVTPART2 im Code abfragen ob es start oder end war. Bei den beiden anderen wird eindeutig auf start|end|alarm getriggert.

Exakte Abfrage auf einen Termin

Jetzt kann man im Code noch exakt abfragen, ob es sich um den richtigen Kalendereintrag handelt.
fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde" limit:count=1,from=0',1)
Die eigentliche Aktion ist damit recht simpel. Wenn die Abfrage des Events exakt genug ist, liefert sie nur bei Übereinstimmung ein Ergebnis. Damit sind alle notwendigen Komponenten komplett.
{fhem("set Server on") if defined fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde|notdienst" limit:count=1,from=0',1)}

Starte das Gerät zum Zeitpunkt

Ein notify, welches zu Beginn eines Kalenderevents, der den Begriff "Sprechstunde" in der Terminbeschreibung enthält, den Server startet:
define n_TestKalender notify TestKalender:changed:.*start {\
fhem("set Server on") if defined fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde" limit:count=1,from=0',1)\
}
Die Sache hat noch einen Schönheitsfehler: Die Sprechstunde beginnt zwar um 8:00 Uhr aber es wäre gut, wenn der Server  schon gestartet ist, wenn alle Mitarbeiter den Dienst beginnen. Dazu brauchen wir den Event nicht exakt zum Termin sondern vorher. Das kann das Calendar Modul erledigen. Mit diesem Attribute wird ein zusätzlicher alarm Event eine Stunde (3600 sec) vorm Termin in Abhängigkeit der Terminbeschreibung erzeugt.
attr TestKalender onCreateEvent { $e->{alarm}= $e->{start}-3600 if($e->{summary} =~ m/Sprechstunde/i)}

Starte das Gerät vor dem Zeitpunkt

Der Trigger im notify muss lediglich von start auf alarm geändert werden, der Ausführungsteil bleibt  identisch.
defmod n_TestKalender notify TestKalender:changed:.*alarm {}
Der Event für start und end bleibt erhalten, zu diesem Zeitpunkt kann man andere Aktionen ausführen.

Noch ein paar zusätzliche Tipps und Infos

Serientermine einer Serie (z.B. jeden Mittwoch 8:00) haben alle die gleiche UID. Bei einem Einzeltermin hätte man im notify über die uid den konkreten Zugriff auf genau den Termin, beim Serientermin erscheinen alle Termine der Serie, auch vergangene! Diese uid bleibt auch erhalten wenn man mal einen Termin der Serie modifiziert (Beschreibung, Zeit usw.)
Die Modi (alarm|start|end|upcoming) sind transient und geben quasi Auskunft über den aktuellen Status eines Termins (get <Kalender> events filter:mode=="<modus>").
  • upcoming - der Termin liegt in der Zukunft
  • end - der Termin ist abgelaufen und liegt in der Vergangenheit
  • start - der Termin ist gerade aktiv
  • alarm - die Alarmphase ist aktiv, also der Event alarm ist vorüber, der Event start noch nicht erreicht.
Achtung: will man auf end triggern und den Termin überprüfen (z.B. Inhalt von summary) dann darf  hideOlderThan nicht auf 0 stehen! Man kann sich leicht mit 1 oder 2 (in sec) behelfen. Nach dem end Event ist der Termin aus der Liste verschwunden und nicht mehr lesbar! 

Noch ein paar Codebeispiele

toDo

Tipp: Die Abfrage der Alarmzeit funktioniert z.B: mit
get TestKalender events filter:mode=="alarm" format:full limit:count=1,from=0
Will man die Tage bis zum nächsten Event wissen (hier einfach der nächste, man kann den Filter natürlich anders setzen). In einer verschachtelten zweiten Abfrage wird dann noch 1 in "morgen" und 0 in "heute" gewandelt.
{my $day = int((fhem('get '.$name.' events format:custom="$t1" limit:from=0,count=1',1) + 86399 - time)/86400);
$day?eval{$day>1?$day:"morgen"}:"heute"}
Will man z.B. die Geburtstage aus dem Google Kontakten in einen ical Kalender kopieren, habe ich hier ein Google Script gefunden. Direkt hat man leider dafür keine ical Adresse verfügbar.

Dienstag, 4. Dezember 2018

Diskpart vs Powershell

Um Festplattenpartitionen zu behandeln - was sollte man nehmen - Diskpart oder Powershell?

Achtung beim Probieren! der clean Befehl innerhalb diskpart wird ohne Nachfrage ausgeführt und löscht die aktive disk!
Die Powershell Befehle fragen zwar nach, Standardantwort ist aber Yes. Also auch hier Vorsicht!

Diskpart lässt sich zwar auch scripten, aber etwas ungewöhnlich. Mit Powershell lässt sich viel mehr machen. Aber die Befehle sind auch zum Teil komplexer.

Ich fange hier einfach mal eine Tabelle, die ich vervollständigen kann:

list disk get-disk|sort number
sel disk 0
list part
Get-Partition -disknumber 0
sel disk 1
clean
Clear-Disk -Number 1 -RemoveData -RemoveOEM
list volume Get-Volume
sel disk 1
(clean)
convert gpt
Initialize-Disk -Number 1
sel disk 1
(clean)
convert mbr
Initialize-Disk -Number 1 -PartitionStyle MBR
sel disk 5
attributes disk clear readonly
Set-Disk -Number 5 -IsReadonly $False






Das geht so richtig gut nur in Powershell

Wo liegt das Laufwerk E?
Get-Partition -DriveLetter E|select DiskNumber,PartitionNumber,DriveLetter,Offset,Size,Type|format-table

Welche Festplatte hat welche Laufwerke?
Get-Volume|Get-Partition |select DiskNumber,PartitionNumber,DriveLetter,Offset,Size,Type|sort DiskNumber,DriveLetter|Format-Table

Erzeuge neues Volume auf Disk 1 mit definiertem DriveLetter
New-Partition -Disknumber 1 -DriveLetter W -UseMaximumSize |Format-Volume

Ein Volume umbenennen
Set-Volume -DriveLetter W -NewFileSystemLabel "Data1"

Sonntag, 18. November 2018

Windows Image

Die Windows Installation (so wie ist) auf eine neue Festplatte ziehen - geht auch mit Boardmitteln.

Achtung! Diese Anleitung enthält Befehle, die bei falscher Anwendung Daten löschen können! Bitte unbedingt zwischen Copy & Paste auch den Kopf verwenden! Dism /capture-image nur innerhalb Windows PE anwenden!

Was wird benötigt?
  • Ein Startmedium mit Windows PE: USB Stick oder CD (ISO)
  • Einen genügend großen Zwischenspeicher: USB Festplatte oder Netzlaufwerk,
  • ein paar Tools von Windows und
  • gewusst wie.
Am Ende dieses Artikels stehen ein paar Links mit wichtigen Quellen!

Ablauf

  1. Den Computer mit Windows PE starten (Man kann Quell und Ziel Laufwerk anschließen)
  2. Die Quell Laufwerk mit dism in ein image "aufzeichnen".
  3. Optional: Umbauen und wieder mit WinPE starten.
  4. Das Ziel Laufwerk partitionieren.
  5. Das Image auf das Ziel Laufwerk "anwenden".
  6. Startsystem wieder herstellen
Es ist also ziemlich simpel, die Befehle und Schritte sind im Prinzip im zweiten Link beschrieben. Ich will dies hier aber mit etwas Erfahrungen und Tipps ausbauen.

Windows PE

Es gibt Windows PE in 32 und 64 bit Version. Obwohl WinPE mit einem einfachen CMD Fenster startet, kann man von dort die meisten (portablen) Programme und Tools einfach starten. Aber - die 64 bit Version hat kein 32 bit Subsystem, man kann also immer nur Programme der gleichen Architektur starten!
Ich habe darauf verzichtet WinPE aufwendig anzupassen. Ich habe einfach ein paar Scripte erstellt, einen Teil habe ich auf dem USB Stick bereitgestellt und Andere einfach auf einem zentralen Netzwerklaufwerk abgelegt.
Es genügt ein kleiner USB2 Stick, WinPE ist nicht groß (ca. 300 MB ohne Anpassung, ca 450 MB mit Powershell) und die Ladezeiten sind nicht sehr lang.
An meinem System lagen die Zeiten zwischen 25 und 30 sec, der Unterschied USB2 und USB3 sowie Powershell ging quasi in der "Nachdenkzeit" des Reset und BIOS unter.

Damit man mit der Tastatur klar kommt braucht man ein Startscript.
wpeutil SetKeyboardLayout 0407:00000407
start cmd
Die Tastatur ist sonst mit englischem Layout und die Änderung mit SetKeyboardLayout wirkt erst im neuen Fenster.
Um eine Netzwerkverbindung herzustellen ist dies sinnvoll. Dieses Zeile enthält auch einen Trick:
Hat man ein Linux SMB Share, welches mit anonymen Zugriff konfiguriert ist, kann sich WinPE nicht ohne Konto verbinden (anonymer Zugriff ist auch in Windows seit Version 10 1709 abgeschaltet).
Bei WinPE hilft es einfach einen Benutzer anzugeben den es nicht gibt: net use ist zufrieden und Linux ignoriert es.
net use * \\server1\Shares /user:willi password 
net use * \\server1\Sicherung  
Natürlich kann man die paar Zeilen auch in ein CMD Script packen und auf dem USB Stick bereitstellen.
Die beiden net use Befehle erzeugen ein Laufwerk z: und y: - das Laufwerk x: ist das Systemlaufwerk von WinPE.

Image aufzeichnen

Zunächst  muss man ermitteln, welche Laufwerksbuchstaben WinPE für die eingehängten Festplatten vergeben hat. Dazu gibt es viele Möglichkeiten, ziemlich eindeutig und gezielt funktioniert es mit diskpart. Am Prompt: DISKPART> gibt man folgende Reihenfolge ein:
list disk
sel disk 0
detail disk
Der erste Befehl zeigt alle eingebauten Festplatten.
Der zweite Befehl selektiert die Quellfestplatte, die Ziffer ist also entsprechend anzupassen.
Der dritte Befehl zeigt alle Details der gewählten Disk inklusive der dort enthaltenen Volumes und Laufwerks Buchstaben.
Tipp: Auch gleich die Nummer für das Zielaufwerk merken, die wird im nächsten Abschnitt benötigt.
Für Fortgeschrittene: Man kann die Befehle für Diskpart auch direkt in der Kommandozeile "pipen".
echo list volume|diskpart
(echo sel disk 0 & echo list part)|diskpart
(echo sel disk 0 & echo sel part 2 & echo detail part)|diskpart
Ein Beispiel zum Aufzeichnen des Images von Laufwerk C
dism /capture-image /imagefile:y:\wim\backup.wim /capturedir:c:\ /name:C-WinOS
Man kann recht einfach ein Beispielscript im Laufwerk z: vorbereiten und mit notepad (läuft unter WinPE) anpassen und dann ausführen. So spart man sich viel Tipparbeit. Die meiste Zeit wird die Bandbreite zum Ziellaufwerk sehr gut genutzt, die Geschwindigkeit für die Aufzeichnung hängt also vom Umfang, Geschwindigkeit Quelle und Netzlaufwerk (oder USB Laufwerk) ab.

Ziel Laufwerk partitionieren

Hinweis: Hat man einen neuen PC mit gleichem Windows OS gekauft, kann man jetzt auch einfach die Windows Partition formatieren, diesen Schritt überspringen und das Image anwenden!

Ein leeres Ziellaufwerk muss man neu partitionieren und kann dabei auch völlig neu strukturiert werden! 
Beispiel: War das Quellsystem eine MBR Partition kann die Ziel Platte ein GPT System werden (falls der PC UEFI unterstützt).
Achtung! Wird jetzt die falsche Disk verwendet, werden ohne Nachfrage Daten gelöscht!
Die Empfehlung von Microsoft (man findet Unterschiedliche) für eine GPT Partitionsstruktur aus meinem zweiten Link sieht als diskpart Script wie folgt aus:
rem == CreatePartitions-UEFI.txt ==
rem == These commands are used with DiskPart to
rem    create four partitions
rem    for a UEFI/GPT-based PC.
rem    Adjust the partition sizes to fill the drive
rem    as necessary. ==
select disk 0
clean
convert gpt
rem == 1. System partition =========================
create partition efi size=100
rem    ** NOTE: For Advanced Format 4Kn drives,
rem               change this value to size = 260 ** 
format quick fs=fat32 label="System"
assign letter="S"
rem == 2. Microsoft Reserved (MSR) partition =======
create partition msr size=16
rem == 3. Windows partition ========================
rem ==    a. Create the Windows partition ==========
create partition primary 
rem ==    b. Create space for the recovery tools ===
shrink minimum=500
rem       ** NOTE: Update this size to match the
rem                size of the recovery tools 
rem                (winre.wim)                    **
rem ==    c. Prepare the Windows partition ========= 
format quick fs=ntfs label="Windows"
assign letter="W"
rem === 4. Recovery tools partition ================
create partition primary
format quick fs=ntfs label="Recovery tools"
assign letter="R"
set id="de94bba4-06d1-4d40-a16a-bfd50179d6ac"
gpt attributes=0x8000000000000001
list volume
exit
Bevor jetzt das Script mit dieser Befehlszeile angewendet wird, ist wie im vorherigen Abschnitt bitte genau zu prüfen auf welche Disk mit welcher Nummer das Ziel geschrieben werden soll! Das Script kann man einfach mit Notepad ändern und speichern.
DiskPart /s Z:\Scripts\CreatePartitions-UEFI.txt

Image anwenden

Für den jetzigen Schritt sind eigentlich alle Vorbereitungen getroffen und man kann direkt loslegen. Ich habe hier das Script zur Anwendung mal etwas vor dem "Doppelklick" entschärft.
@if "%1" == "" goto usage 
dism /Apply-Image /ImageFile:%1 /Index:1 /ApplyDir:%2
goto end
:usage
@echo Nichts passiert: Parameter fehlen, bitte so verwenden
@echo apply y:\wim\backup.wim W:\
:end

Startsystem erzeugen

Jetzt muss man mindestens noch die neue Platte "start-fähig" machen. Dazu genügt in der Regel ein Befehl. Wieder als ungefährliches Script:
@if "%1" == "" goto usage 
bcdboot %1 /s %2
goto end
:usage
@echo Nichts passiert: Parameter fehlen, bitte so verwenden
@echo makeboot W:\Windows S:
:end
Jetzt kann man mit dem Befehl
wpeutil shutdown
das System herunterfahren, den USB Stick und die Quellplatte entfernen und das System sollte sich mit der neuen Platte einfach starten lassen.

Recovery System erzeugen

Eigentlich fehlt noch etwas, das Recovery System. Vielleicht war es auf der Quellplatte schon nicht vorhanden, aber bei einer Windows Installation gehört es dazu. Solange die Laufwerke aus Schritt 4 noch vorhanden sind, sollte dieses Script das Recovery System einrichten.
@if "%1" == "" goto usage 
md %1
copy %2\System32\Recovery\winre.wim %1\winre.wim
%2\System32\reagentc /setreimage /path %1 /target %2
goto end
:usage
@echo MakeRecovery R:\Recovery\WindowsRE W:\Windows
:end
Anmerkung:

  • Man kann das Recovery System auch später im laufenden System einrichten, aber dann muss man noch einmal einen Laufwerksbuchstaben zuweisen und wieder entfernen.
  • Der Befehl reagentc erzeugt unter WinPE keinerlei Ausgabe, d.h. /info hat keine Wirkung.
  • Es kann erforderlich sein unter dem fertigen Windows noch einmal den Schalter /enable anzuwenden! Die obige Aktion richtet das Recovery Image zwar ein, aktiviert es aber nicht.

Windows PE bekommen

Windows PE kann man aus unterschiedlichsten Quellen verwenden und "bauen". Ich habe einfach das aktuelle Windows ADK heruntergeladen, man braucht die Deployment Tools und das Windows PE AddOn (Details im 1. Link, aktuelles ADK im 4. Link))
Das "Kommandocenter" für die Umgebung öffnet man nach der Installation mit "Windows + Bereit (tippen)" + rechte Maustaste "Als Administrator ausführen".
Hier braucht man nur zwei Befehle, Laufwerk mit 600-800 MB freiem Platz und einen freien USB Stick. Die Laufwerksbuchstaben und Pfade müssen angepasst  werden.
Achtung, der USB Stick wird (nach Nachfrage) neu formatiert!
copype amd64 D:\WinPE_amd64
MakeWinPEMedia /UFD D:\WinPE_amd64 Q:
Man kann natürlich auch ein ISO File erstellen, z.B. für Test in Hyper-V.
MakeWinPEMedia /ISO D:\WinPE_amd64 D:\ISO\WinPE_amd64.iso
Relativ simpel kann man das Start Script von WinPE erweitern:
Dism /Mount-Image /ImageFile:"C:\WinPE_amd64\media\sources\boot.wim" /index:1 /MountDir:"C:\WinPE_amd64\mount"
# an der Stelle das Script editieren C:\WinPE_amd64\mount\Windows\System32\Startnet.cmd
Dism /Unmount-Image /MountDir:"C:\WinPE_amd64\mount" /commit

Anmerkung für Dual Boot x86 und amd64:
Ich hatte hier schon mal einen dualen Windows Setup Stick gebaut. Ob die Anleitung mit aktuellem ADK funktioniert habe ich nicht getestet.

Probleme lösen

Ich hatte unklare, nicht reproduzierbare Probleme mit der 32 bit Version von WinPE und dism /Capture-Image. Ich habe daraufhin konsequent alles mit der 64 bit Version getestet. Ob die 32 bit Version ein generelles Problem hat kann ich nicht sagen.
Abbruch von dism
Beim Prozess "Aufzeichnen" hatte ich neben zu wenig Platz auf dem Netzlaufwerk Probleme die mit spartanischen Fehlermeldungen von dism abgebrochen wurden. dism schreibt aber normal eine Logdatei nach \Windows\Logs\DISM.
War der Vorgang erfolgreich sollte man die übrigens löschen, sonst ist sie beim nächsten Mal und einer Fehlersuche unnötig groß.
Durch die Hinweise im Log konnte ich die Fälle lösen.
  • chkdsk /f half beim Fehler mit dem Volumebit auf dem Laufwerk.
  • Löschen einer Datei im User Ordner \OneDrive half, weil dism offenbar eine "kaputte" Datei erst aus der Cloud komplett synchronisieren wollte.
  • Ein kleine verdächtige Datei im Browsercache des Users wurde vom Virenscanner erkannt. Ich hatte zu dem Zeitpunkt nicht WinPE gestartet sondern das normale System laufen und die Platte zusätzlich angesteckt.
Ich war durchaus froh über diese "Fehler" die sonst mit auf die neue Platte gewandert wären.
Der Virenscanner im laufenden System wird den Aufzeichnungsprozess verlangsamen! Aber man sollte durchaus erstmal das System auf der originalen Platte gründlich scannen!

Neue Platte startet nicht
Trotz identischem Vorgehen, hatte ich ein UEFI System, welches zunächst nicht starten wollte. Nachdem ich etwas konfus probiert und untersucht habe, war die Lösung ziemlich einfach:
  • Einfach solange in den Fehler starten (3 mal) bis Windows von sich aus in die Startoptionen startet. 
  • Dort einmal die Option: "Abgesichert mit Kommando Fenster" auswählen, anmelden und dann neu starten. Windows repariert so offenbar sein Startsystem automatisch.

Recovery System funktioniert nicht
Die obigen Befehle funktionieren scheinbar alle einwandfrei, aber im fertigen System meldet der Befehl
reagentc /info
das Recovery System als nicht verfügbar.
Der Befehl zum Aktivieren bringt eine Fehlermeldung
reagentc /enable
REAGENTC.EXE: Startkonfigurationsdaten können nicht aktualisiert werden.
Nach langer erfolgloser Suche habe ich einfach das probiert:
del c:\Windows\System32\Recovery\ReAgent.xml
Danach funktionierte /enable einwandfrei.

Zum Schluss

Ich bin oft für spartanische Lösungen, mit dem was gerade da ist. Windows hat seit langem schon alles an Board um eine Systeminstallation (die Systempartition) zu sichern und auf eine neue Festplatte zu bringen. Man braucht keine "bunten" Tools, die im Zweifelsfall auch bloß Dinge machen die man nicht versteht und die Fehlerhaft sein können.
Was ich hier aufgeschrieben habe funktioniert Schritt für Schritt "zu Fuß" und wenn man will mit Batch- oder Powershellscripts. Die c't hat vor Jahren schon mal ein Script für dism & Co vorgestellt welches noch weiter geht. Es erstellt eine Sicherung, die sich mit Setup wieder installieren lässt. (WIMage)
Diese Methode hat gegenüber dem üblicherweise verwendeten sektorbasiertem 1:1 Image durchaus Vorteile:
  • Dateisystem wird quasi auf Fehler überprüft
  • Quell und Zielsystem können unterschiedlich sein:
    • BIOS / UEFI
    • Partitionsgröße - man muss z.B. nicht vorher zeitaufwendig defragmentieren und verkleinern um ein System von einer großen Partition auf eine kleiner SSD zu migrieren
    • Partitionsanordnung
  • man könnte Ordner aus dem Aufzeichnungsprozess ausschließen (WimScript.ini - [ExclusionList])
  • unterschiedliches Dateisystem - muss ich noch untersuchen?
  • Zielsystem ist anschließend erstmal defragmentiert.

Nützliche Tools für WinPE

Die Anleitung zur Integration von Powershell findet man im dritten Link. Die Startzeit vergrößert sich dadurch etwas. 
Ein aktueller OpenSource Dateimanager mit 32 und 64 bit: DoubleCommander  
Ansonsten laufen relativ viele OpenSource Programme ohne Probleme.

Wichtige Quellen


  1. Microsoft: Boot to Windows PE
  2. Microsoft Aufzeichnen und Anwenden von wim Images
  3. Microsoft WinPE mit Powershell
  4. Microsoft: Aktuelles ADK


Dienstag, 30. Oktober 2018

Dateiversionsverlauf oder?

... FileHistory - die Übersetzung von eindeutigen und meist kurzen englischen Begriffen endet oft in sehr kunstvollen Umschreibungen.
Mit Windows 11 hat es sich ein wenig geändert: Man will in Zukunft nur noch OneDrive als Backup in den Vordergrund stellen. 

Windows Sicherung

Windows bietet dem Anwender eine Absicherung seines Systems in zwei Stufen:
Die eigene Dateien -> Mit Dateiversionsverlauf sichern - zu finden in Menü Einstellung/Sicherung.
Das System -> kann man "Wiederbeleben" durch Reset, Recovery, neues Setup.
Die beiden Dinge sind vom normalen Anwender halbwegs beherrschbar, zumindest die Einrichtung und dann braucht er sich eigentlich nicht mehr kümmern.
Der Insider kann auch noch mit dism ein Image seines gesamten Systemes ziehen.

Montag, 15. Oktober 2018

Powershell und Windows Update

Der Hyper-V Server hat ein Management Interface welches aus mehreren Scripts besteht:
Auszug

  • sconfig.cmd
    • sconfig.vbs
    • WUA_SearchDownloadInstall.vbs
    • ...
Die VBS-Scripts liegen %systemroot%\system32\en-us\ (bzw. anderen Sprachen).
Gesteuert wird das Windows Update über die Windows Update API. Die Doku ist schwerer Stoff, ich habe mir von verschiedenen Seiten und aus dem oben erwähnten Script ein paar Dinge zusammen gelesen und will es hier kurz notieren.
Meine Scripts in dem Artikel sind alle nicht perfekt, sondern eher als Lösungsansatz gedacht. Damit kann man z.B. beim Hyper-V Server etwas mehr tun als nur mit der sconfig "Oberfläche".

Die Schritte etwas im Detail

Zuerst muss man festlegen welche Updates man suchen will (ein paar Details):
$Criteria = "IsInstalled=0 and Type='Software'"
$Criteria = "IsInstalled=0 and Type='Driver'"
Dann werden verschiedene Com Objecte eingerichtet und z.B. die Titel der verfügbaren Updates angezeigt.
$Searcher = New-Object -ComObject Microsoft.Update.Searcher
$SearchResult = $Searcher.Search($Criteria).Updates
# Titel anzeigen
$SearchResult|select Title
Powershell bietet an der Stelle etwas mehr Komfort zur Suche und Auswahl als die eigentliche API. Deshalb füge ich hier bewusst den Neubau der Update Collection ein. Eigentlich ist diese jetzt schon in $SearchResult enthalten.
$updatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl
$SearchResult|%{ if($_.Title -match "KB2267602") {$updatesToDownload.Add($_)}}
$updatesToDownload|select Title
Danach werden die gewünschten Updates heruntergeladen.
$Session = New-Object -ComObject Microsoft.Update.Session
$Downloader = $Session.CreateUpdateDownloader()
$Downloader.Updates = $updatesToDownload
$Downloader.Download()
Um es ganz korrekt zu machen, kann man noch eine Collection der wirklich heruntergeladenen Updates erstellen ...
$updatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl
$SearchResult|%{ if($_.isDownloaded) {$updatesToInstall.Add($_)}}
... um dann die Updates zu installieren.
$Installer = New-Object -ComObject Microsoft.Update.Installer
$Installer.Updates = $updatesToInstall
$Result = $Installer.Install()
Als Abschluss wird bei Bedarf ein Neustart ausgeführt.
If ($Result.rebootRequired) { shutdown.exe /t 0 /r }

Komplett in einem Script

Das Ganze als ein Script mit zwei Parametern am Anfang:
Den Type entweder weglassen (Alle suchen) oder auf Software oder Driver setzen.
Bei $compare kann man entweder nur einen "*" für "Alles" setzen oder wie im Beispiel den Begriff mit Wildcards für ein bestimmtes Update.
# Parameter
$Criteria = "IsInstalled=0 and Type='Software'"
$compare= "*" # "*KB2267602*"
# Suche notwendige Updates 
$Searcher = New-Object -ComObject Microsoft.Update.Searcher
$SearchResult = $Searcher.Search($Criteria).Updates
# Zusammenstellung Download Collection
$updatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl
$SearchResult|%{ if( $_.Title -like $compare ) {$updatesToDownload.Add($_)}}
#$updatesToDownload|select Title
#$updatesToDownload.Count
# Download Updates 
$Session = New-Object -ComObject Microsoft.Update.Session
$Downloader = $Session.CreateUpdateDownloader()
$Downloader.Updates = $updatesToDownload
if ($updatesToDownload.Count -gt 0) {$Downloader.Download()}
# Zusammenstellung Install Collection
$updatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl
$SearchResult|%{ if($_.isDownloaded) {$updatesToInstall.Add($_)}}
# Install Updates 
$Installer = New-Object -ComObject Microsoft.Update.Installer
$Installer.Updates = $updatesToInstall
$Result = $Installer.Install()
# Neustart wenn gefordert
If ($Result.rebootRequired) { shutdown.exe /t 0 /r }

Was lief bisher?

Man kann sich auch die gesamte Historie anzeigen lassen:
Wann wurde welches Update installiert? Die Zeiten werden in UTC angezeigt!
# Historie abfragen
$session = new-object -comobject Microsoft.Update.Session
$searcher = $session.CreateUpdateSearcher()
$history = $searcher.QueryHistory(0, $searcher.GetTotalHistoryCount())
$history |select Date,Title |more
Leider liefert das Ergebnis am Ende immer viele Leerzeilen.

Mehr Komfort

Die Powershell Gallery halt ein umfangreiches Scriptmodul "PSWindowsUpdate" bereit mit dem der Windows Update Service wohl sehr komfortabel behandelt werden kann. Ich habe mir das zunächst nur kurz angeschaut. Um das Script aus der PSGallery zuinstallieren muss man ein paar Vorbereitungen treffen, das ist hier ganz gut beschrieben.

Noch ein Special

In $SearchResult stehen auf den zweiten Blick noch mehr Informationen!
So erzeugt man eine Liste mit Detailinformationen, z.B. mit der UpdateID oder dem Download Link.
#$Criteria = "IsInstalled=0 and Type='Software'"
$Searcher = New-Object -ComObject Microsoft.Update.Searcher
$SearchResult = $Searcher.Search($Criteria).Updates

foreach ($update in $SearchResult) {
  $title = $update.Title
  $guid = $update.Identity.UpdateId
  $title
  $guid
  $bundles = $update.BundledUpdates
     foreach($bundledUpdate in $bundles) {
        foreach($content in $bundledUpdate.DownloadContents) {
           if (!$content.IsDeltaCompressedContent) {
              $url = $content.DownloadUrl
              $url
            }
        }
    }
}

Mit der so ermittelten UpdateID wäre es auch möglich die Suche nach speziellen Paketen auszuführen:
$Criteria = "IsInstalled=0 and UpdateID='a3fafd03-b687-49e6-9df9-3963057ce376'"

Es gibt mit CIM und "PowerShell remoting session" auch einen Ansatz die VMs zentral zu patchen.
Ich habe hier ein Infos z.B. zu den resultcodes gefunden.