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
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.
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
Allgemeiner Ablauf
- 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.
- Bei Powershell wird aus username:password noch ein extra "Credential" String gemacht, da Invoke-Webrequest die url nicht einfach so verarbeitet wie curl.
- Der csrfToken wird extrahiert und gespeichert.
- 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
- Zum Schluss wird eine Schleife über das Befehlsarray abgearbeitet, die Befehle url-encoded und an den FHEM Server übergeben und die Antwort ausgewertet.
- 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