Mittwoch, 22. August 2018

QR Code für WiFi Zugang

Ohne viel zu tippen und eventuell falsche Zahlen und Buchstaben kommt man mit modernen Android und IOS Systemen per Scan ins Wlan. WiFi Zugang (WLAN) teilen heisst der Menüpunkt im Smartphone wenn man mit dem Wlan verbunden ist. Und das Smartphone zeigt in dem Moment genau einen QR Code, den man mit dem anderen Smartphone scannen kann und mit einem Klick kann man sich verbinden.

Also einfach einen QR Code fürs Wlan basteln!

Es gibt dazu einen standardisierten String der bereitgestellt werden muss. Die einzelnen Textelemente sind durch ; getrennt. Die "" können normalerweise entfallen.

WIFI:T:WPA;P:"password";S:"SSID Name";;

Unter debian Linux

sudo apt-get install qrencode
Unter OpenWrt gibt es auch das Paket qrencode allerdings mit Einschränkungen.

Diese Befehlszeile erzeugt die PNG Datei:

qrencode -t png -o ~/SSID_wifi_config.png 'WIFI:T:WPA;P:"password";S:"SSID Name";;'


Oder unter OpenWrt erzeugt man eine SVG Datei, da ist kein PNG verfügbar!

qrencode -t svg -o ~/SSID_wifi_config.svg 'WIFI:T:WPA;P:"password";S:"SSID Name";;'

Die fertige Datei drucken, oder anderweitig zur Verfügung stellen.

Mit dem "QR Code Reader - Ohne Werbung" von Sustainable App-Developer hatte ich guten Erfolg unter Android. Er will nur Bilder aufnehmen und braucht keine weiteren Rechte.

Freitag, 17. August 2018

Mit OpenWrt Presence Informationen ermitteln

Der zentrale Router weiß ja eigentlich am Besten was los ist im Netzwerk. Ich habe mal die Idee verfolgt auf dem OpenWrt einen Prozess zu starten, der aktuelle Information von Netzwerkgeräten nach "aussen" übermittelt. Als erstes Beispiel will ich mal einen bestimmten Wlan Client ermitteln.
Da die finale kurze Abfrage Schleife leicht unübersichtlich ist, erkläre ich erstmal die einzelnen Komponenten.

Ein paar Grundlagen

Je nach Wlan Hardware hat man unterschiedliche Tools um das Wlan abzufragen. Bei mir war iw und iwinfo vorhanden. Ich muss noch prüfen ob der Befehl ip auch (und ganz universell) zu gebrauchen wäre.
Die Ausgabe wird dann für die weitere Verarbeitung gefiltert. Diese beiden Befehle liefern das gleiche Ergebnis:
iwinfo |grep -oE "wlan\d-\d|wlan\d"
iw dev | grep Interface | cut -f 2 -s -d" "
Eine Liste der im Router definierten Wlan Netzwerke.
wlan0
wlan1
wlan1-1
wlan1-2
Eine List der angemeldeten Clients bekommt man mit einem dieser beiden Befehle:
iwinfo wlan1-1 assoclist
iw dev wlan1-1 station dump
In einer Schleife über alle Wlan Netzwerke und einem Ausgabe Filter bekommt man eine komplette Liste der MAC Adressen der aktiven Wlan Clients:
for w in $(iwinfo |grep -oE "wlan\d-\d|wlan\d"); do
   iwinfo $w assoclist | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}'
done

Abfrage einer bestimmten MAC Adresse

Jetzt alles zusammengefasst mit einer Abfrage eine bestimmten MAC Adresse und als Resultat die Übergabe an einen Dummy in FHEM (API WEB ohne csrf Token!).
Das Script /root/loop.sh
#Die Verzögerungszeit der Abfrageschleife kann auch übergeben werden
WATCHDOG_SLEEP_SEC=${1:-2}
#Url zu FHEM
u=http://192.168.56.80:8088/fhem?cmd=set%20WL_Mi6%20
MAC_ADDRESS_1="80:AD:xx:xx:xx:xx"

while sleep $WATCHDOG_SLEEP_SEC; do
 if (
      for m in $(
                 for w in $(iwinfo |grep -oE "wlan\d-\d|wlan\d")
                 do
                   iwinfo $w assoclist | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}'
                 done
                )
      do
        [ "$m" = "$MAC_ADDRESS_1" ] && exit 0
      done
      exit 1
    )
 then
   c=on
 else
   c=off
 fi

 # tue nur etwas wenn sich c geändert hat
 if [ "$c1" != "$c" ] ; then wget -qs $u$c ; c1=$c ; fi

done

Automatischer Start

Jetzt fehlt noch der automatische Start in OpenWrt. Dafür ist procd zuständig. Ich habe dazu ein init Script mit minimaler Funktion erstellt (siehe auch Erklärung unter dem Text):
cat <<EOF > /etc/init.d/loop
#!/bin/sh /etc/rc.common

USE_PROCD=1

START=99
STOP=01

start_service() {
    procd_open_instance
    procd_set_param command /bin/sh "/root/loop.sh"
    procd_set_param stdout 1
    procd_set_param stderr 1
    procd_close_instance
}
EOF 
Achtung! ich lerne ja selbst immer wieder dazu und habe diesen Code mal bewusst als Here Doc  dargestellt. Wenn man den Text, inklusive der ersten und letzten Zeile, einfach in die Eingabe des Terminals wirft, wird automatisch die Datei am richten Ort erzeugt, ohne das man einen Editor bemühen muss! 
Wer das nicht will, der kopiert den Text ohne die erste und letzte Zeile in den Editor seiner Wahl.

Das Ganze muss noch ausführbar gemacht und aktiviert werden:
chmod +x /etc/init.d/loop
/etc/init.d/loop enable
/etc/init.d/loop start 
Mit dem Befehl service kann man überprüfen ob der Dienst angekommen ist und ihn auch steuern.
root@wrt1900:~# service
service "" not found, the following services are available:
boot              dropbear          linksys_recovery  network           sysfixtime        uhttpd
cron              firewall          lm-sensors        odhcpd            sysntpd           umount
dnsmasq           gpio_switch       log               rpcd              system            urandom_seed
done              led               loop              sysctl            ucitrack

War es das schon?

Es gibt natürlich weitere Ideen:

  • Umstieg auf Eventbasierte Abarbeitung ähnlich wie im verlinkten Beispiel.
  • Konfigurierbare Abfrage mehrerer Clients
  • Auslesen aller Host und Übergabe an FHEM 
Aber jetzt will ich erstmal diesen Prozess eine Weile Probe laufen lassen und ein Monitoring des Routers einrichten.

Mittwoch, 8. August 2018

Lokale Namen OpenWrt

Mit der Fritzbox war es immer etwas mystisch: Wann klappt die Namens Auflösung von lokalen Geräten? Am Ende war nur noch Konfiguration mit IP Adressen anstatt Namen angesagt. Blöd wenn man mal etwas umstellen will. Mit OpenWrt sollte es doch jetzt alles klar beherrschbar sein!?

Erkenntnisse

Scheinbar werden von dnsmasq die lokalen Namen erst geliefert, wenn die DHCP Clients die Lease aktualisiert haben. Theoretisch überleben die Leases auch einen Neustart des Routers. Ich hatte aber oft die Situation, da war nach dem Start alles erstmal weg. Die Leasetime steht per Standard auf 12 h. Da dauert es dann ganz schön lange, wenn ein Client mal die Lease erneuern will. Irgendwo hatte ich mal gelesen, dass passiert nach der halben Leasetime (6 h). Dafür muss eine Lösung her.
Frage: Wo packt man jetzt Hostnamen hin, damit sie sofort zur Verfügung stehen?
Antwort: /etc/hosts

Code

Die static Leases sind schlecht lesbar wenn sie nur die IP und Mac enthalten, der Hostname sollte da schon mit in die Liste. Damit man nicht alles doppelt halten und konfigurieren muss, wäre es jetzt sinnvoll die static Leases einfach auszulesen und in die hosts Datei zu schreiben.
Teile des Codes habe ich aus dem Ubuntu Wiki.
Die Abschnitte im Code sind entsprechend kommentiert.

#!/bin/ash

# Perform work in temporary files
temphosts1=`mktemp`
temphosts2=`mktemp`
dom=$(uci get dhcp.@dnsmasq[0].domain)

# If this is our first run, save a copy of the system's original hosts file and set to read-only for safety
if [ ! -f ~/hosts-system ]
then
 echo "Saving copy of system's original hosts file..."
 cp /etc/hosts ~/hosts-system
 chmod 444 ~/hosts-system
fi

# Read Hostnames from DHCP
for i in $(uci show dhcp | grep -oE "host\[\d+\].ip"|grep -oE '\d+'); do
  ip=$(uci get dhcp.@host[$i].ip)
  name=$(uci get dhcp.@host[$i].name)
   if [ ! -z $name ]
   then
    echo $ip $name.$dom $name >>$temphosts2
   fi
done

echo -e "\n# Ad hosts from dhcp "`date` | cat ~/hosts-system - $temphosts2 > $temphosts1

cp $temphosts1 /etc/hosts

# Clean up temp files and remind user to copy new file
echo "Cleaning up..."
rm $temphosts1 $temphosts2

# The File must be readable for everyone
chmod 644 /etc/hosts

#Restart Service
/etc/init.d/dnsmasq restart
echo "Done."


Dienstag, 31. Juli 2018

Import static Leases in OpenWrt

OpenWrt kann mit speziellen Kommandos (uci) in der Shell konfiguriert werden.
Ich habe mir immer gewünscht, statische DHCP Reservierungen einfach durch Textdateien zu konfigurieren. OpenWrt kann zwar wenigsten in der Weboberfläche die MAC Adressen per Copy&Paste einfügen, aber ich hätte es gern komfortabler.
Wer spielen will, der macht zunächst nichts kaputt. Die Konfigurationsänderungen werden zunächst nur temporär ausgeführt.
Die dhcp Konfiguration anzeigen:
uci show dhcp
uci show dhcp | grep '@host\[0\]'
uci get dhcp.@host[0].ip
Die Host Einträge in der /etc/config/dhcp stehen dort am Ende und sind nicht indiziert. Das uci indiziert sie "fließend" für uns und einen direkten Zugriff:
  • [0] ist der erste, 
  • [-1] der letzte Eintrag.

Vorbemerkung

Ich habe ein paar Scripts als heredoc erstellt, die kann man einfach per copy&paste ins Terminal fallen lassen. Damit die $ Zeichen nicht als Variablen interpretiert werden, muss EOF in einfache ' ' gesetzt werden!
Die uci arbeitet temporär, man kann sich mit uci changes den Erfolg anzeigen lassen. Mit uci revert dhcp kann man alles immer wieder rückgängig machen.
Erst ein uci commit schafft Tatsachen.

Hosts hinzufügen

Will man einen neuen Host eintragen geht das mit folgendem Beispiel. Ein neuer Eintrag wird angefügt und ist damit der Letzte, dieser wird um die Werte ip und mac ergänzt:
script="addhost.sh"
cat <<'EOF' >$script
#!/bin/sh
if [ $# -eq 3 ]
  then
    echo "name=$1 ip=$2 mac=$3"
  else
    echo "Fehler: Bitte name ip und mac angeben."
    exit
fi
uci add dhcp host
uci set dhcp.@host[-1].name=$1
uci set dhcp.@host[-1].ip=$2
uci set dhcp.@host[-1].mac=$3
EOF
chmod +x $script
Jetzt kann man eine existierende Lease Zeile aus dem Browser kopieren und als Reservierung einfügen
./addhost.sh HPF43909586B94 192.168.56.111 F4:39:09:58:6B:94
Mit einer Schleife kann man Zeilenweise den stdin lesen und und pro Zeile einen neuen Hosteintrag erstellen. Damit die static Leases lesbar bleiben sollte ein Hostname mit in die Tabelle.
Read Host per Line -> rhpl.sh
script="rhpl.sh"
cat <<'EOF' >$script
#!/bin/sh
while read line; do
./addhost.sh $line
done
EOF
chmod +x $script
Dazu eine Datei mit Wertepaaren -> wp.txt
host1 192.168.1.1 aa:bb:cc:dd:ee:d1
host2 192.168.1.2 aa:bb:cc:dd:ee:d2
host3 192.168.1.3 aa:bb:cc:dd:ee:d3
Das Ganze mit der Pipe verknüpft und fertig ist der Import!
./rhpl.sh <wp.txt
Nachtrag 2021: Hat man mehrere Dateien, kann man diese gemeinsam lesen, die MAC Adressen in Großbuchstaben wandeln, sortieren, Doubletten entfernen und einlesen
cat leases.txt wpo.txt |awk '{ print( $1,$2,toupper($3) ) }'|sort|uniq|./rhpl.sh
Noch ist nichts aktiv!

Hosts löschen

Will man vor dem Import aufräumen, kann man mit einem (nicht perfekten) Einzeiler erst einmal alle Hosts löschen:
while uci delete dhcp.@host[0]; do :;done
Die Schleife läuft einmal zu viel, deswegen gibt es einen Fehler. Aber alle Hosts sind gelöscht.

Host auslesen

Damit man die Liste der Werte Paar nicht komplette per Hand erstellen muss, kann man existierende Daten auslesen. Die entstandenen Dateien kann man bei Bedarf bearbeiten und anschließend importieren.

Static Leases auslesen

Um alle Hosts in eine Datei zu exportieren, braucht man eine formatierte Ausgabe
printf "$(uci get dhcp.@host[-1].name) $(uci get dhcp.@host[-1].ip) $(uci get dhcp.@host[-1].mac)\n"
Und ein Array mit den vorhandenen Indizes. Ein doppelter "Eierkuchen" liefert uns dies aus dem show Befehl.
echo $(uci show dhcp | grep -oE "host\[\d+\].ip"|grep -oE '\d+')
Eine simple Schleife liefert uns die Wertepaare als Ausgabe.
script="wrlp.sh"
cat <<'EOF' > $script
#!/bin/sh
for i in $(uci show dhcp | grep -oE "host\[\d+\].ip"|grep -oE '\d+'); do
  printf "$(uci get dhcp.@host[$i].name) $(uci get dhcp.@host[$i].ip) $(uci get dhcp.@host[$i].mac)\n"
done
EOF
chmod +x $script
Die schreibt man einfach in eine Datei.
./wrlp.sh >wpo.txt

Aktuelle Leases auslesen

Nachtrag 2021: Ich habe noch eine bessere Variante zum Auslesen der aktiven Leases gefunden, lasse aber den alten Code bewusst noch stehen.
cat /tmp/dhcp.leases| awk '$4 != "*" { print( $4,$3,$2 ) }'
Um einfach mal alle existierenden Leases auszulesen genügt ein Einzeiler. Allerdings ist das Ausgabeformat anders als im Beispiel oben: mac IP Host (Darstellung im Browser Host IP mac)
cat /tmp/dhcp.leases |grep -oE '([a-f0-9]{2}:){5}[a-f0-9]{2}\s[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}\s[A-Z,a-z,0-9,-,_]*'
Das Script macht ein match auf mac Adresse, IP Adresse und String (Hostname)

Ich habe noch eine vereinfachte Form "experimentell" gefunden (\s.*\s passt auf IP Adresse und Hostname - warum? Der Rest wird abgeschnitten).
Aus der Ausgabe kann man noch per Pipe mit grep die Zeilen mit unbekannten Hostnamen (*) entfernen und mit awk formatieren.

grep -oE '([a-f0-9]{2}:){5}[a-f0-9]{2}\s.*\s' /tmp/dhcp.leases|grep -v "*"|awk '{print $3,$2,$1}'

Finale

Das uci System arbeitet temporär, will man seine Änderungen fest machen, muss man diese speichern und das jeweilige System(dhcp) neu starten. 
Achtung: Scheinbar wird seit einiger Zeit nicht mehr dnsmasq sondern odhcpd für den DHCP Service verwendet. Offenbar genügt aber ein commit Befehl den restart & reload kann man sparen?
uci commit dhcp
/etc/init.d/dnsmasq restart
luci-reload

Die uci Schnittstelle ist auch über JSON erreichbar.
wiki.teltonika.lt

Ersatz für ssh-copy-id

Manchmal kommt es vor, das auf einem System das Tool ssh-copy-id nicht vorhanden ist. Dann gibt es zwar einfache Alternativen, aber wenn man nicht genau weiß worauf es ankommt, wird es schnell kompliziert. Ich mache hier raus mal einen kurzen Artikel, weil ich auch ein paar Besonderheiten der Shell gelernt habe. 
Für die Verwendung unter Windows habe ich ein Powershellscript auf GitHub abgelegt.

Die einzelnen Schritte

Was macht ssh-copy-id bzw. was muss man berücksichtigen?
Alles in allem geht es darum, den Public Key von System 1 (lokal) nach System 2 (remote) zu übertragen um sich zukünftig vom System 1 am System 2 vereinfachten anzumelden und Befehle auszuführen.
  • lokalen Public Key an das Remotesystem senden.
  • im Remotesystem schauen ob der Pfad .ssh schon existiert, 
    • wenn nicht: Pfad anlegen.
  • in diesem Pfad schauen ob schon eine Datei .ssh/id_rsa.pub existiert, 
    • wenn nicht: mit dem Public Key erzeugen.
    • wenn schon vorhanden: prüfen ob der public Key schon enthalten ist,
      • wenn nicht: Den Public Key hinzufügen.
Wer Hemmungen hat den Programmcode zu verwenden, kann die oben genannten Schritte auch manuell mit ls und nano (Public Key einfach mit copy&paste übertragen) abarbeiten.
 Schon die erste Aufgabe ist nicht trivial, hier wird mit der Pipe gearbeitet und in Abhängigkeit der Shell stehen dort unterschiedliche Subsysteme zur Verfügung. Variablen können nicht einfach transportiert werden.
Hinweise:

  • Ich habe die Codestücke zur Erklärung ohne Maskierung geschrieben. So funktionieren sie aus der Windows cmd Konsole bzw. direkt in der Shell. Wie weiter unten erläutert muss $ und " maskiert werden!
  • der letzte Befehl (cat bzw. echo) ist nur zum Test ob es funktioniert hat.
Am Einfachsten geht es in eine Datei.
cat .ssh/id_rsa.pub | ssh user@host "cat >kee.tmp;cat kee.tmp"
In Windows sind für Linux oft schädliche Zeilumbrüche (CR/LF) enthalten, dies Variante filtert die CR aus.
type .ssh\id_rsa.pub | ssh user@host "cat | tr -d '\r' >kee.tmp;cat kee.tmp"
Nach einigen Versuchen glaube ich mittlerweile, dass es mit diesem Konstrukt auch zuverlässig in eine Variable ($pub) kommt:
cat .ssh/id_rsa.pub | ssh user@host "pub=$(cat );echo $pub"
type .ssh\id_rsa.pub | ssh user@host "pub=$(cat | tr -d '\r');echo $pub"
Ob das Verzeichnis existiert ist schnell getestet, das ODER führt den Befehl danach nur bei false aus. Vorher wird mit dem umask Befehl die richtige Berechtigung für den neuen Pfad sichergestellt.
umask 077;test -d .ssh || mkdir .ssh
Auf ähnliche Art wird die Datei getestet und bei false einfach erzeugt:
test -f .ssh/authorized_keys || cat kee.tmp >.ssh/authorized_keys
test -f .ssh/authorized_keys || echo $pub >.ssh/authorized_keys
Danach wird der Inhalt der Datei mit grep auf Vorhandensein des Key geprüft und bei false wird der public Key angehängt. Bei der ersten Variante wird die tmp datei gelöscht.
Da Leerzeichen im Public Key sind, braucht grep den Suchstring in "ein zwei drei"!
grep -q "$(cat kee.tmp)" .ssh/authorized_keys  || cat kee.tmp >>.ssh/authorized_keys;rm kee.tmp
grep -q "$(echo $pub)" .ssh/authorized_keys  || echo $pub >>.ssh/authorized_keys

Die Einzeiler

Da die Dateinamen im Code öfters gebraucht werden, definiere ich sie am Anfang als Variable. Jetzt steht noch zusätzlich die Aufgabe den Einzeiler so zu schreiben, dass alle Sonderzeichen von der Shell richtig behandelt werden. Die folgenden Varianten haben als Ziel ein Linux System! Startet man den Befehl aus der Linux Shell muss mit dem Backslash \ maskiert werden:
cat .ssh/id_rsa.pub | ssh user@host "p='.ssh';akey=\$p'/authorized_keys'; okey='kee.tmp'; cat >\$okey; umask 077; test -d \$p || mkdir \$p; test -f \$akey || cat \$okey >\$akey; grep -q \"\$(cat \$okey)\" \$akey  || cat \$okey >>\$akey;rm \$okey"
cat .ssh/id_rsa.pub | ssh user@host "p='.ssh';akey=\$p'/authorized_keys'; pub=\$(cat ); umask 077; test -d \$p || mkdir \$p; test -f \$akey || echo \$pub >\$akey; grep -q \"\$(echo \$pub)\" \$akey || echo \$pub >>\$akey"
Startet man man den Einzeiler aus der cmd von Windows, müssen lediglich die " verdoppelt werden:
type .ssh\id_rsa.pub | ssh user@host "p='.ssh';akey=$p'/authorized_keys'; pub=$(cat | tr -d '\r'); umask 077; test -d $p || mkdir $p; test -f $akey || echo $pub >$akey; grep -q ""$(echo $pub)"" $akey || echo $pub >>$akey"
Baut man den Befehl in Powershell zusammen muss mit dem Backtick ` maskiert werden.

Der Pfad .ssh gilt für die meistens Standard Systeme mit Benutzeranmeldung. Bei vielen Spezialsystemen (Zielsystem) muss der Pfad angepasst werden!
z.B. /etc/dropbear bei OpenWrt.
cat .ssh/id_rsa.pub | ssh user@host "p='/etc/dropbear';akey=\$p'/authorized_keys'; pub=\$(cat ); umask 077; test -d \$p || mkdir \$p; test -f \$akey || echo \$pub >\$akey; grep -q \"\$(echo \$pub)\" \$akey || echo \$pub >>\$akey"
Die nächste Variante hat als Ziel ein Windows System!
ziel='user@host';pkey=$(cat ~/.ssh/id_rsa.pub);ssh $ziel "findstr /c:\"$pkey\" .ssh\authorized_keys ||mkdir .ssh ||echo $pkey >>.ssh\authorized_keys"

Maskierung von Sonderzeichen
Windows: Nur " muss verdoppelt werden -> ""
Linux: $ und " müssen mit \ maskiert werden -> \$ \"
Powershell: $ und " müssen mit ` maskiert werden -> `$ `"

Mittwoch, 18. Juli 2018

Kalender in FHEM einbinden

Die älter Bereitstellung des Link habe ich bereits hier gezeigt. In der neuen Form des Google Kalenders gibt es den grünen iCal Button nicht mehr, der Link steht jetzt unter Einstellungen/<Kalendername>/Kalendereinstellungen und dort weit unten in der Box:
Privatadresse im iCal Format
Darunter steht der wichtige Hinweis:
Mit dieser Adresse können Sie von anderen Anwendungen aus auf den Kalender zugreifen, ohne ihn öffentlich zu machen.

Das Kalender Modul in FHEM ist aktuell überarbeitet und die Funktionen sind erweitert und geändert worden. Mittlerweile weiß ich, dass es keine gute Lösung ist ein Modul einzusetzen, welches auf einem anderen Modul aufsetzt aber völlig getrennt von dem entwickelt wird. Man schafft unnötig Abhängigkeiten.
Bei der Einbindung eines Kalenders in FHEM sollte man sich über den praktischen eigenen Umgang mit dem Kalender und dem Aktualisierungsintervall (Standard 1 h) Gedanken machen. Ein Abfallkalender der sich das ganze Jahr praktisch nicht ändert muss entweder nie oder höchsten 1 mal Tag aktualisiert werden.

1. Beispiel: 

Signalisierung der Abfalltonne, Aktualisierung einmal am Tag
define AbfallKalender Calendar ical url https://... 86400
Dann brauchen wir ein Gerät wo die aktuelle Tonne drin steht, im einfachsten Fall ein Dummy
define Tonne dummy
Und ein Timer der einmal am Tag den Kalender ausliest und das Ergebnis ablegt.
define a_Tonne at *12:00:00 set Tonne {(my $evt=fhem('get AbfallKalender events format:custom="$S" limit:from=1d,to=1d');;;;$evt?$evt:"0")}
Die Funktion:
  • Am Mittag den Ganztagestermin für morgen auslesen -> limit:from=1d,to=1d
  • Nur den Textinhalt des Eintrages lesen -> format:custom="$S"
  • $evt?$evt:"0" -> Falls kein Termin gefunden wird, wird der dann leere String durch eine 0 ersetzt.
  • Den resultierenden Text in den Dummy Tonne schreiben.
Damit kann man am Vortag des Termines ein Aktion auslösen: z.B. Nachmittag den Hinweis geben: die Tonne muss morgen raus. Und am Morgen des Termines kann man nochmal den Hinweis geben: die Tonne muss heute raus. Der Zeitpunkt im Timer entscheidet über das Auftauchen und Verschwinden des Inhaltes von der "Tonne".
Hinweis:
Bei Perl Code innerhalb von einem at Kommando müssen die ; verdoppelt werden, im define müssen sie auch verdoppelt werden, deshalb ;;;; !
Bei Perl Code im set Befehl muss eine zusätzliche Klammer () stehen, sonst wird der Perlausdruck als String gesehen: set Tonne {3+5} ergibt {3+5} im Dummy, set Tonne {(3+5)} ergibt 8 im Dummy.

2. Beispiel: 

Im Kalender stehen FHEM Device Namen (z.B. Aktoren), die Terminzeiten sind die Schaltzeiten.
Das Beispiel gilt für Einzeltermine (eigene uid). Serientermine haben eine uid für alle Termine, hier muss man weiter filtern(limit:from ...).
Der Kalender mit stündlicher Aktualisierung:
define TestKalender Calendar ical url https://... 
Dann wiederum ein Dummy:
define Urlaub dummy
Und ein notify, welches exakt zu Beginn (start) und Ende (end) des Einzeltermines den Dummy auf on oder off setzt. (Code für die Raw Def)
define n_TestKalender notify TestKalender:changed:.* {\
  my $cmd ='on';;\
  $cmd = 'off' if ($EVTPART2 eq 'end');;\
  my $actor = fhem('get '.$NAME.' events format:custom="$S" filter:uid=="'.$EVTPART1.'" limit:count=1');;\
  fhem("set $actor $cmd");;\
}

Funktion
Das Calendar Modul wirft zum Zeitpunkt des Termines ein paar Events, davon spricht das notify genau auf diese Beiden an:
2018-07-16 16:25:00 Calendar TestKalender changed: 123456googlecom start
...
2018-07-16 16:30:00 Calendar TestKalender changed: 123456googlecom end
Der Event hat drei Teile:
  • $EVTPART0 ist uninteressant, 
  • $EVTPART1 enthält die ID des Eintrages und 
  • $EVTPART2 "start" bzw. "end".

Mit Hilfe der ID wird der Kalendertext gelesen und mit den umgewandelten "on" (start) und "off" (end) Befehlen das Device in FHEM gesetzt.
Warum sieht der Befehl so kompliziert aus?

  • Der uid/format String muss im String die doppelten Anführungszeichen enthalten! Damit dies funktioniert, muss der gesamte Befehl in ' ' gesetzt werden. Innerhalb werden dann zwar " akzeptiert aber keine Variablen mehr aufgelöst. Diese muss man hier mit Verkettung/concatenation einbauen.

Weiter unten habe ich eine (für den Anfänger) besser lesbare Variante eingebaut.

Komplexen Code auslagern

Für komplexeren Code sollte man alles in die 99_myUtils auslagern und im notify lediglich die wichtigen Parameter übergeben:
define n_TestKalender notify TestKalender:changed:.* {KalenderSub($EVTPART1,$EVTPART2,$NAME)} 
Das folgende Beispiel filtert das Ereignis (Devicenamen) "Urlaub" aus dem Kalender und setzt das gleichlautendes Device bei Start und Ende auf ja /nein. Alle anderen Events werden zwar getriggert aber verworfen.
Die sub kann man beliebig komplex gestaltet. Damit der get Befehl lesbar bleibt, habe ich alles in extra Variablen gepackt. (Code für die 99_myUtils.pm)
sub KalenderSub ($$$)
   {
     my ($uid,$cmd,$cname) = @_;
     #Logging der Paramter bei Bedarf;     #Log 1, "uid: $uid | Start/End: $cmd | cal: $cname";
     my $format = '"$S"';
     my $dev = 'Urlaub';
     $uid='"'.$uid.'"';
     $cmd = 'ja' if ($cmd eq 'start');
     $cmd = 'nein' if ($cmd eq 'end');
     
     my $actor = fhem("get $cname events format:custom=$format filter:uid==$uid limit:count=1");
     # Unterschiedliche Abfragen auf ist exakt oder enthält möglich;
     #fhem("set $actor $cmd") if ($actor eq $dev);
     if ($actor =~ /$dev/) {fhem("set $dev $cmd")};
}

Tipp

Der Calendar Aufruf wird mit Level 3 geloggt, wenn man das nicht möchte hängt man an den Befehlsaufruf einfach noch ",1" an.
fhem("Befehl",1)

Weitere Infos:
Forum
Wiki

Samstag, 7. Juli 2018

Etwas Powershell für ssh

Alle Hostverbindungen sind in lnk Dateien definiert und jetzt wäre es cool wenn man diese Definitionen vielfach weiterverwendet.
In Ergänzung zu meinem letzten Artikel will ich hier ein paar Bausteine aufzeigen und die Gedanken dahinter und den Syntax kurz erklären.

Zunächst mal ein paar grundlegende Variablen und Objekte damit der Code universell bleibt.
# Varianten für Zielpfad
#$HomePath = "$env:HOMEDRIVE$env:HOMEPATH\Desktop"
$HomePath = "$env:onedrive\ssh Hosts"
# Filter
$Filter = "*.lnk"
$FArg = "@"

# Shell Object zum Zugriff auf Eigenschaften von lnk
$shell = New-Object -COM WScript.Shell
Kurz und kompakt ist die Variante mit konsequenter Ausnutzung von Pipes.

  • GCI liefert alle Dateien mit der Endung lnk in einem Pfad.
  • Für jedes Element wird ein Shell Objekt erzeugt und im Element Arguments nochmal gefiltert. Im Beispiel auf das Zeichen @ um wirklich nur die lnk Dateien mit user@host zu behandeln. Im Element Targetpath steht der Befehl komplett mit Pfad.
  • Für alle gefilterten Elemente wird ein neuer Prozess mit cmd gestartet und einfach das Kommando aus der lnk Datei aufgeführt.

Ergo werden alle vorhandenen "ssh" Hostdefinitionen in separaten Fenstern geöffnet.
# Variante mit Pipes
Get-childItem $HomePath -filter $Filter |
  ForEach-Object {$shell.CreateShortcut($_.FullName)}|where Arguments -match $FArg|
    ForEach-Object {
       Start-Process "cmd.exe" "/c $($_.Targetpath) $($_.Arguments)"
    }
Die beiden Elemente Targetpath und Arguments liegen als eine Art Objectreferenz vor. Damit wirklich der Inhalt als String verwendet wird muss dies im Process Aufruf mit $() aufgelöst werden.
Es geht auch ohne Pipes, dann werden alle Übergänge in Variablen gespeichert, der Ablauf ist gleich.
# Variante ohne Pipes
$Files = Get-childItem $HomePath -filter $Filter
ForEach ($File in $Files) {
   $lnkfile = $shell.CreateShortcut($File.FullName) 
   if ($($lnkfile.Arguments) -match $FArg ) {
      Start-Process "cmd.exe" "/c $($lnkfile.Targetpath) $($lnkfile.Arguments)"
   }
}
Für andere Fälle kann man auch einfach ein Array mit Hosts erstellen und lässt eine ForEach Schleife über dieses Array laufen.
$Hosts = @("192.168.178.80","192.168.178.81","192.168.178.82","192.168.178.83")
ForEach ($h in $Hosts) {}
Um den Code leserlich und modular zu halten, kann man zu Beginn alles in Variablen packen. Die Unterbringung von Befehlssquenzen in Variablen ist nicht ganz trivial. Damit man gut testen kann und nicht immer den ganzen Code verändern muss, packe ich  auch das erste cmd Argument in eine String Variable $cs.
#Absoluter Pfad zum Public Key
$Keyfile = "$env:HOMEDRIVE$env:HOMEPATH\.ssh\id_rsa.pub"
$cs = "/C "  # Schließt das cmd Fenster nach Abarbeitung
#$cs = "/K "  # Lässt das cmd Fenster offen - nur für Tests wichtig!
$sshTarget = "pi@$h"
Die eigentliche Codezeile ( Ersatz ssh-copy-id) enthält zwei in sich geschachtelte Argumentelisten. Zunächst der Fenstertyp für cmd.exe in $cs.
Die Kombination aus $cs, $C1 und $sshTarget wäre für sich ausführbar, Ergebnis ist eine Shellkonsole.
Danach kommt die Argumentenliste (Befehle) die in der Shellkonsole ausgeführt werden sollen. Diese muss komplett in " " verpackt werden. Innerhalb dieser Argumentenliste werden wieder Variablen mit dem $ Zeichen im Namen gebildet. Diese Namen haben nichts mit der aufrufenden Powershell zu tun.
Damit Powershell die Zeichen $ und " nicht auflöst, muss man sie mit Backticks (`) schützen. Wenn man die Befehlsfolge an den Stellen aufteilt, wo man sie auch separat testen könnte, bleibt sie lesbar und ist gut zu dokumentieren. Der Code funktioniert, im eigentlichen Artikel gibt es eventuell eine aktuelle Variante.
$C1 = "type $Keyfile | ssh " 
$C2 = " akey='.ssh/authorized_keys'; okey='keee.tmp'; cat | tr -d '\r' >`$okey;" # Variablen erzeugen, Text aus der Pipe ohne CR speichern
$C3 = " umask 077; test -d .ssh || mkdir .ssh;" # Berechtigung setzen, Verzeichnis prüfen sonst anlegen
$C4 = " test -f `$akey || cat `$okey >`$akey;" # Datei prüfen sonst direkt anlegen
$C5 = " grep -q `"`"`$(cat `$okey)`"`" `$akey  || cat `$okey >>`$akey;rm `$okey" # Prüfen ob Key schon vorhanden sonst hinzufügen 
Der grep Befehl bekommt einen String übergeben der in sich durch " geschützt sein muss. Damit die Shell die " nicht auflöst müssen sie in der Shell durch Verdopplung geschützt werden, damit Powershell die " im String nicht auflöst müssen sie durch ` geschützt werden.
Jetzt fügen wir alles zusammen und testen das Ergebnis als Ausgabe. Hier erfolgt jetzt das oben erwähnte Einpacken der Shellbefehle in " . Man kann $C2 bis $C5 flexibel weglassen und einzeln testen.
$Argument = $($cs + $C1 + $sshTarget + " `"" + $C2 + $C3 + $C4 + $C5 + "`"")
Write-Output $Argument
Wenn alles gut aussieht kann man den Befehl testen.
Start-Process "cmd.exe" $Argument
Jetzt kann man alles zusammenpacken und auf Knopfdruck entweder alle Hosts auf einmal in getrennten ssh Fenstern öffnen oder gleich alle Hosts mit einem Vorgang mit den eigenen Public Keys versorgen.
Hier eine komplette Version mit ein paar Optionen, die durch Kommentarzeichen noch aus- oder eingeschaltet werden müssen.
# Varianten für Zielpfad
#$HomePath = "$env:HOMEDRIVE$env:HOMEPATH\Desktop"
$HomePath = "$env:onedrive\ssh Hosts"

# eigener Public Key
$Keyfile = "$env:HOMEDRIVE$env:HOMEPATH\.ssh\id_rsa.pub"
# Filter 
$Filter = "*.lnk"
$FArg = "pi@"
# cmd Fenstertyp bestimmen
$cs = "/C"  # Schließt das cmd Fenster nach Abarbeitung
#$cs = "/K"  # Lässt das cmd Fenster offen - nur für Tests wichtig!

# Kommando für cmd Aufruf in Einzelteilen
$C1 = " type $Keyfile | ssh " 
$C2 = " akey='.ssh/authorized_keys'; okey='keee.tmp'; cat | tr -d '\r' >`$okey;" # Variablen erzeugen, Text aus der Pipe ohne CR speichern
$C3 = " umask 077; test -d .ssh || mkdir .ssh;" # Berechtigung setzen, Verzeichnis prüfen sonst anlegen
$C4 = " test -f `$akey || cat `$okey >`$akey;" # Datei prüfen sonst direkt anlegen
$C5 = " grep -q `"`"`$(cat `$okey)`"`" `$akey  || cat `$okey >>`$akey;rm `$okey" # Prüfen ob Key schon vorhanden sonst hinzufügen 

# Die Alternativen Zeilen für OpenWrt Router
#$FArg = "root@"
#$C2 = " akey='/etc/dropbear/authorized_keys'; okey='keee.tmp'; cat | tr -d '\r' >`$okey;"

# Shell Object zum Zugriff auf Eigenschaften von lnk
$shell = New-Object -COM WScript.Shell
# Variante mit Pipes
Get-childItem $HomePath -filter $Filter |
  ForEach-Object {$shell.CreateShortcut($_.FullName)}|where Arguments -match $FArg|
    ForEach-Object {
       $sshTarget = $_.Arguments
       # Entweder nur die Public Keys übertragen 
       $Argument = $($cs + $C1 + $sshTarget + " `"" + $C2 + $C3 + $C4 + $C5 + "`"")
       # oder nur die Terminalsession öffnen
       #$Argument = "$cs $($_.Targetpath) $($_.Arguments)"
        
       Write-Output $Argument # Zum Test
       #Start-Process "cmd.exe" $Argument # eigentlicher Befehl 
    }