fhem-mirror/FHEM/00_Neuron.pm
klausw dc2da5c16a 00_Neuron.pm: fixed typo
git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@24903 2b470e98-0d58-463d-a4d8-8e2adae1ed80
2021-08-31 21:03:32 +00:00

1033 lines
36 KiB
Perl

##############################################
# $Id$
#
# bug?: wenn man einen value setzt, dann wird im response der alte zurückgeschickt
#
#"alias": "al_Versuch1"
#{"glob_dev_id": 1, "dev": "input", "circuit": "1_01", "value": 0, "mode": "Simple", "counter_modes": ["Enabled", "Disabled"], "modes": ["Simple", "DirectSwitch"], "debounce": 50, "counter": 0, "counter_mode": "Enabled"},
#{"glob_dev_id": 1, "dev": "relay", "circuit": "1_01", "value": 0, "mode": "Simple", "modes": ["Simple", "PWM"], "pending": false, "relay_type": "digital"},
#{"glob_dev_id": 1, "dev": "relay", "circuit": "2_05", "value": 0, "mode": "Simple", "modes": ["Simple"], "pending": false, "relay_type": "physical"},
#{"glob_dev_id": 1, "dev": "ai", "circuit": "1_01", "value": 0.0104..09, "unit": "V", "mode": "Voltage", "range_modes": ["10.0"], "modes": ["Voltage", "Current"], "range": "10.0"},
#{"glob_dev_id": 1, "dev": "ai", "circuit": "2_01", "value": -0.001..28, "unit": "V", "mode": "Voltage", "range_modes": ["0.0", "2.5", "10.0"], "modes": ["Voltage", "Current", "Resistance"], "range": "10.0"},
#{"glob_dev_id": 1, "dev": "ao", "circuit": "1_01", "value": 0.0, "unit": "V", "mode": "Voltage", "modes": ["Voltage", "Current", "Resistance"]},
#{"glob_dev_id": 1, "dev": "ao", "circuit": "2_01", "value": 0.0, "unit": "V", "mode": "Voltage", "modes": ["Voltage"]},
#{"glob_dev_id": 1, "dev": "led", "circuit": "1_01", "value": 0},
#{"glob_dev_id": 1, "dev": "wd", "circuit": "1_01", "value": 0, "timeout": 5000, "was_wd_reset": 0, "nv_save": 0},
#{"glob_dev_id": 1, "dev": "neuron","circuit": "1", "ver2": "1.0", "sn": 31, "model": "M503", "board_count": 2},
#{"glob_dev_id": 1, "dev": "uart", "circuit": "1_01", "conf_value": 14, "stopb_modes": ["One", "Two"], "stopb_mode": "One", "speed_modes": ["2400bps", "4800bps", "9600bps", "19200bps", "38400bps", "57600bps", "115200bps"], "parity_modes": ["None", "Odd", "Even"], "parity_mode": "None", "speed_mode": "19200bps"}]#
#{"vis": "0", "dev": "temp", "circuit": "2620531402000075", "typ": "DS2438", "lost": false, "temp": "24.25", "interval": 15, "vad": "2.52", "humidity": 50.196646084329984, "vdd": "5.34", "time": 1527144341.185264}
#
package main;
use strict;
use warnings;
require "HttpUtils.pm";
require "DevIo.pm";
my @clients = qw(
NeuronPin
);
my %opcode = ( # Opcode interpretation of the ws "Payload data
'continuation' => 0x00,
'text' => 0x01,
'binary' => 0x02,
'close' => 0x08,
'ping' => 0x09,
'pong' => 0x0A
);
my %setsP = (
'off' => 0,
'on' => 1,
);
#my %rsetsP = reverse %setsP;
sub Neuron_Initialize(@) {
my ($hash) = @_;
eval "use JSON;";
return "please install JSON::XS" if($@);
eval "use Digest::SHA qw(sha1_hex);";
return "please install Digest::SHA" if($@);
# Provider
$hash->{Clients} = join (':',@clients);
$hash->{MatchList} = { "1:NeuronPin" => ".*" };
$hash->{ReadFn} = "Neuron_Read";
$hash->{ReadyFn} = "Neuron_Ready";
$hash->{WriteFn} = "Neuron_Test";
$hash->{DefFn} = 'Neuron_Define';
$hash->{UndefFn} = 'Neuron_Undef';
$hash->{ShutdownFn} = 'Neuron_Undef';
$hash->{SetFn} = 'Neuron_Set';
$hash->{GetFn} = 'Neuron_Get';
$hash->{AttrFn} = 'Neuron_Attr';
$hash->{NotifyFn} = 'Neuron_Notify';
$hash->{AttrList} = "connection:websockets,polling poll_interval "
."wsFilter:multiple-strict,ai,ao,input,led,relay,wd,temp,unit_register "
."logicalDev:multiple-strict,ai,ao,input,led,relay,wd,temp,unit_register "
."$readingFnAttributes";
return undef;
}
sub Neuron_Define($$) {
my ($hash, $def) = @_;
my @parts=split("[ \t][ \t]*", $def);
return "Usage: define <name> Neuron <hostname|ip>[:<tcp-portnr>]" unless defined $parts[2];
$hash->{NOTIFYDEV} = "global";
my ($address, $port) = split(/:/, $parts[2]);
$port = "80" unless defined $port;
$hash->{HOST} = $address;
$hash->{PORT} = $port;
$hash->{DeviceName} = $address.":".$port;
$hash->{STATE} = "defined";
return undef;
}
sub Neuron_Undef(@){
my $hash = shift;
Neuron_Close($hash);
RemoveInternalTimer($hash);
return undef;
}
sub Neuron_Set(@) {
my ($hash, $name, $cmd, @args) = @_;
my $sets = $hash->{HELPER}{SETS};
if ($hash->{HELPER}{SETS} && index($hash->{HELPER}{SETS}, $cmd) != -1) { # dynamisch erzeugte outputs
my ($dev, $circuit) = Neuron_GetExt($hash, $cmd);
#my ($dev, $circuit) = (split '_', $cmd, 2);
my $value = (looks_like_number($args[0]) ? $args[0] : $setsP{$args[0]});
if ($hash->{HELPER}{wsKey} && DevIo_IsOpen($hash)) {
my $string = Neuron_wsEncode('{"cmd":"set", "dev":"'.$dev.'", "circuit":"'.$circuit.'", "value":"'.$value.'"}');
Neuron_Write($hash,$string);
} else {
Neuron_HTTP($hash,$dev,$circuit,$value);
}
} elsif ($cmd eq "postjson") {
my ($dev, $circuit , $value, $state) = @args;
$value = '{"'.$value.'":"'.$state.'"}' if (defined($state));
$hash->{HELPER}{CLSET} = $hash->{CL};
Neuron_HTTP($hash,$dev,$circuit,$value);
} elsif ($cmd eq "websocket") {
if ($args[0] && $args[0] eq 'open') {
Neuron_Open($hash);
} else {
Neuron_Close($hash);
}
} elsif ($cmd eq "clearreadings") {
fhem("deletereading $hash->{NAME} .*", 1);
} else {
return "Unknown argument $cmd, choose one of clearreadings:noArg websocket:open,close " . ($hash->{HELPER}{SETS} ? $hash->{HELPER}{SETS} : '');
}
return undef;
}
sub Neuron_Get(@) {
my ($hash, $name, $cmd, @args) = @_;
if ($cmd eq "all") {
Neuron_GetAll($hash);
} elsif ($cmd eq "updt_sets_gets") {
Neuron_ReadingstoSets($hash);
} elsif ($cmd eq "value") {
if (index($hash->{HELPER}{GETS}, $args[0]) != -1) {
my ($dev, $circuit) = Neuron_GetExt($hash, $args[0]);
$hash->{HELPER}{CLVAL} = $hash->{CL};
Neuron_HTTP($hash, $dev, $circuit);
} else {
return "Unknown Port $args[0], choose one of ".$hash->{HELPER}{GETS};
}
} elsif ($cmd eq "conf") {
if (index($hash->{HELPER}{GETS}, $args[0]) != -1) {
my ($dev, $circuit) = Neuron_GetExt($hash, $args[0]);
$hash->{HELPER}{CLCONF} = $hash->{CL};
Neuron_HTTP($hash, $dev, $circuit);
} else {
return "Unknown Port $args[0], choose one of ".$hash->{HELPER}{GETS};
}
} else {
my @gets = ('updt_sets_gets:noArg','all:noArg');
if ($hash->{HELPER}{GETS}) {
push(@gets, 'value:' . $hash->{HELPER}{GETS});
push(@gets, 'conf:' . $hash->{HELPER}{GETS});
}
return "Unknown argument $cmd, choose one of " . join(" ", @gets);
}
return undef;
}
sub Neuron_GetExt(@) { # bei Extensions mithilfe von ExtensionNamen splitten (geht sonst bei Typen mit "_" wie unit_register schief)
my ($hash, $pinname) = @_;
my $n = 2;
while (exists($hash->{$n."_CIRCUIT"})){
my $pos = index($pinname, $hash->{$n."_CIRCUIT"});
if ( $pos != -1) {
return (substr($pinname,0,$pos - 1), substr($pinname,$pos));
}
$n +=1;
}
return (split '_', $pinname, 2);
}
sub Neuron_Attr(@) {
my ($cmd, $name, $attr, $val) = @_;
# $cmd - Vorgangsart - kann die Werte "del" (löschen) oder "set" (setzen) annehmen
# $name - Gerätename
# $attr/$val sind Attribut-Name und Attribut-Wert
my $hash = $defs{$name};
if ($attr && $attr eq 'connection') {
if ($val && $val eq 'websockets' && $cmd eq 'set') {
Log3 $hash, 5, "Neuron_Attr oeffne WS";
Neuron_Open($hash);
} else {
Log3 $hash, 5, "Neuron_Attr schließe WS";
Neuron_Close($hash);
}
} elsif ($attr eq 'poll_interval') {
if ( defined($val) ) {
if ( looks_like_number($val) && $val > 0) {
RemoveInternalTimer($hash);
if (AttrVal($hash->{NAME}, 'connection', 'polling') eq 'polling') {
InternalTimer(1, 'Neuron_Poll', $hash, 0);
} else {
return '$hash->{NAME}: poll intervall can\'t defined together with websocket connection';
}
} else {
return "$hash->{NAME}: Wrong poll intervall defined. poll_interval must be a number > 0";
}
} else {
RemoveInternalTimer($hash);
}
} elsif ($attr eq 'wsFilter') {
Neuron_wsSetFilter($hash,$val);
}
return undef;
}
sub Neuron_Poll($) {
my ($hash) = @_;
my $name = $hash->{NAME};
if (AttrVal($hash->{NAME}, 'connection', 'polling') eq 'polling') {
# Read all values
Neuron_GetAll($hash);
my $pollInterval = AttrVal($hash->{NAME}, 'poll_interval', 0);
InternalTimer(gettimeofday() + ($pollInterval * 60), 'Neuron_Poll', $hash, 0) if ($pollInterval > 0);
}
}
sub Neuron_Notify(@) {
my ($hash, $nhash) = @_;
my $name = $hash->{NAME};
return '' if(IsDisabled($name));
my $events = deviceEvents($nhash, 1);
if($nhash->{NAME} eq "global" && grep(m/^INITIALIZED|REREADCFG$/, @{$events}))
{
Neuron_ReadingstoSets($hash);
Neuron_forall_clients($hash,\&Neuron_Init_Client,undef);
}
return undef;
}
sub Neuron_forall_clients($$$) {
my ($hash,$fn,$args) = @_;
foreach my $d ( sort keys %main::defs ) {
if ( defined( $main::defs{$d} )
&& defined( $main::defs{$d}{IODev} )
&& $main::defs{$d}{IODev} == $hash ) {
&$fn($main::defs{$d},$args);
}
}
return undef;
}
sub Neuron_Init_Client($@) {
my ($hash,$args) = @_;
if (!defined $args and defined $hash->{DEF}) {
my @a = split("[ \t][ \t]*", $hash->{DEF});
$args = \@a;
}
my $name = $hash->{NAME};
Log3 $name,5,"im init client fuer $name ";
my $ret = CallFn($name,"InitFn",$hash,$args);
if ($ret) {
Log3 $name,2,"error initializing '".$hash->{NAME}."': ".$ret;
}
}
###########################################################################################################
sub Neuron_Test($$) {
my ( $hash, @args) = @_;
my ($dev, $circuit , $value, $state) = @args;
Log3($hash, 4,"$hash->{TYPE} ($hash->{NAME}) from logical dev: @args");
# if ($hash->{HELPER}{WESOCKETS} && looks_like_number($value)) {
if (looks_like_number($value) && $hash->{HELPER}{wsKey} && DevIo_IsOpen($hash)) {
#my $string = Neuron_wsEncode('{"cmd":"set", "dev":"'.$dev.'", "circuit":"'.$circuit.'", "value":"'.$value.'"}');
my $string = '{"cmd":"set", "dev":"'.$dev.'", "circuit":"'.$circuit.'", "value":"'.$value.'"}';
Log3($hash, 4,"$hash->{TYPE} ($hash->{NAME}) from logical dev to Websocket: $string");
Neuron_Write($hash,Neuron_wsEncode($string));
} else {
if (defined($state)) {
if ($value eq 'debounce' || $value eq 'counter' || $value eq 'pwm_duty' || $value eq 'pwm_freq') { #debounce Werte dürfen nicht in Hochkommas sein
$value = '{"'.$value.'":'.$state.'}';
} elsif ($value eq 'counter_mode') {
$value = '{"'.$value.'":'.lc($state).'}';
}else {
$value = '{"'.$value.'":"'.$state.'"}';
}
}
Log3($hash, 4,"$hash->{TYPE} ($hash->{NAME}) from logical dev to HTTP: $dev,$circuit".($value ? ",$value" : '').($state ? ",$state" : ''));
Neuron_HTTP($hash,$dev,$circuit,$value);
}
return undef;
}
#####################################
# http fuctions
#####################################
sub Neuron_HTTP(@){
my ($hash,$dev,$circuit,$data) = @_;
#my $url="http://$hash->{HOST}:$hash->{PORT}/json/$dev/$circuit";
my $url="http://$hash->{HOST}:$hash->{PORT}/".(defined($data) ? "json" : "rest")."/$dev/$circuit";
if (defined($data) && index($data, ':') == -1) {
unless ($dev eq 'ao') {
$data = '{"value":"'.$data.'"}';
} else {
$data = '{"value":'.$data.'}'; # Sonderlösung, da der Analoge Ausgang den Wert nur ohne Hochkommas akzeptiert
}
}
Log3($hash, 4,"$hash->{TYPE} ($hash->{NAME}): sending ".($data ? "POST ($data)" : "GET")." request to url $url");
my $param= {
url => $url,
hash => $hash,
timeout => 30,
method => ($data ? "POST" : "GET"),
data => ($data ? $data : ''),
header => "User-Agent: fhem\r\nAccept: application/json",
parser => \&Neuron_ParseSingle,
callback => \&Neuron_callback
};
HttpUtils_NonblockingGet($param);
return undef;
}
sub Neuron_GetAll(@){
my ($hash) = @_;
#my $url="http://$hash->{HOST}:$hash->{PORT}/json/all";
my $url="http://$hash->{HOST}:$hash->{PORT}/rest/all";
Log3($hash, 4,"$hash->{TYPE} ($hash->{NAME}): sending GET all request with url $url");
my $param= {
url => $url,
hash => $hash,
timeout => 30,
method => "GET",
header => "User-Agent: fhem\r\nAccept: application/json",
parser => \&Neuron_ParseAll,
callback => \&Neuron_callback
};
HttpUtils_NonblockingGet($param);
return undef;
}
#####################################
# functions to handle responses
#####################################
sub Neuron_callback(@) {
my ($param, $err, $data) = @_;
my ($hash) = $param->{hash};
if($err){
Log3($hash, 3, "$hash->{TYPE} ($hash->{NAME}) received callback with error:\n$err");
readingsSingleUpdate($hash,"state",$err,1);
} elsif($data){
Log3($hash, 5, "$hash->{TYPE} ($hash->{NAME}) received callback with:\n$data");
if( substr($data,0,6) eq "<html>" ) {
(my $title) = $data =~ /\<title\>(.*)\<\/title\>/;
readingsSingleUpdate($hash,"state",$title,1);
} else {
my $parser = $param->{parser};
&$parser($hash, $data);
asyncOutput($hash->{HELPER}{CLCONF}, $data) if $hash->{HELPER}{CLCONF};
}
#delete $hash->{HELPER}{CLCONF};
} else {
Log3($hash, 2, "$hash->{TYPE} ($hash->{NAME}) received callback without Data and Error String!!!");
readingsSingleUpdate($hash,"state","no data received",1);
}
delete $hash->{HELPER}{CLCONF};
return undef;
}
sub Neuron_ParseSingle(@){
my ($hash, $data)=@_;
my $result;
Log3($hash, 4, "$hash->{TYPE} ($hash->{NAME}) parse data:\n".$data);
eval {
$result = JSON->new->utf8(1)->decode($data);
#Log3 ($hash, 1, "$hash->{TYPE} ($hash->{NAME}) single result->status=".ref($result));
};
if ($@) {
Log3 ($hash, 3, "$hash->{TYPE} ($hash->{NAME}) error decoding response: $@");
readingsSingleUpdate($hash,"state","JSON decode error",1);
} elsif ( $result->{status} && $result->{status} eq 'fail' ) {
readingsSingleUpdate($hash,"state",'fail',1);
asyncOutput($hash->{HELPER}{CLSET}, "set response fail") if $hash->{HELPER}{CLSET};
Log3 ($hash, 2, "$hash->{TYPE} ($hash->{NAME}) http response fail with: ".$result->{data});
} else {
readingsSingleUpdate($hash,"state",'success',1);
my %addvals = (STATUS => $result->{status}) if exists $result->{status};
my $data;
if (exists $result->{data}) {
if (exists $result->{data}{result}) {
$result = $result->{data}{result};
} else {
$result = $result->{data};
}
} elsif (exists $result->{result}) {
$result = $result->{result};
} else {
$result = $result;
}
if (ref $result eq 'HASH') {
if (exists $result->{address}) {
# {"interval": 15, "value": 23.7, "circuit": "VlTiba", "address": "28751F1F0C000067", "time": 1624541181.043978, "typ": "DS18B20", "lost": false, "dev": "temp"}
# Besonderheit für 1Wire, dort ist im circuit scheinbar das alias enthalten
readingsSingleUpdate($hash,$result->{dev}."_".$result->{address},$result->{value},1);
} else {
readingsSingleUpdate($hash,$result->{dev}."_".$result->{circuit},$result->{value},1);
}
asyncOutput($hash->{HELPER}{CLVAL}, $result->{value}) if $hash->{HELPER}{CLVAL};
delete $hash->{HELPER}{CLVAL};
Dispatch($hash, $result, (%addvals ? \%addvals : undef)) if index(AttrVal($hash->{NAME}, 'logicalDev', 'relay,input,led,ao,temp,unit_register') , $result->{dev}) != -1;
} else {
Log3 ($hash, 3, "$hash->{TYPE} ($hash->{NAME}) http response not JSON: ".$result);
}
}
delete $hash->{HELPER}{CLSET};
return $result;
}
sub Neuron_ParseAll(@){
my ($hash, $data)=@_;
my $result;
Log3($hash, 5, "$hash->{TYPE} ($hash->{NAME}) parse data:\n$data");
eval {
$result = JSON->new->utf8(1)->decode($data);
};
if ($@) {
Log3 ($hash, 3, "$hash->{TYPE} ($hash->{NAME}) error decoding response: $@");
readingsSingleUpdate($hash,"state","JSON decode error",1);
} else {
###################################################################
eval {
#Log3 ($hash, 1, "$hash->{TYPE} ($hash->{NAME}) result->status=".ref($result));
my %addvals = (STATUS => $result->{status}) if ref $result eq 'HASH';
my ($subdevs) = (ref $result eq 'HASH' ? $result->{data} : $result);
readingsBeginUpdate($hash);
my $i = 1;
my @circuits;
foreach (@{$subdevs}){
(my $subdev)=$_;
if (defined $subdev->{model} && defined $subdev->{glob_dev_id} && $subdev->{glob_dev_id} == 1) {
foreach my $intrnl (keys %{$subdev}) {
next if $intrnl eq "glob_dev_id";
$hash->{uc($intrnl)} = $subdev->{$intrnl};
}
if ($subdev->{board_count}) {
for my $i (1..$subdev->{board_count}) {
push (@circuits,$i);
}
}
} elsif (defined $subdev->{model} && defined $subdev->{glob_dev_id}) {
foreach my $intrnl (keys %{$subdev}) {
next if $intrnl eq "glob_dev_id";
$hash->{$subdev->{glob_dev_id} . "_" . uc($intrnl)} = $subdev->{$intrnl};
}
push (@circuits, substr($subdev->{circuit}, -1));
} elsif (defined $subdev->{model}) {
foreach my $intrnl (keys %{$subdev}) {
next if $intrnl eq "glob_dev_id";
$hash->{'ext'.$i.'_'.uc($intrnl)} = $subdev->{$intrnl};
}
$i++;
} else {
my $value = $subdev->{temp}; # Temperaturwert nehmen (!wire Geräte haben kein value?)
$value = $subdev->{value};
#$value = $rsetsP{$value} if ($subdev->{dev} eq 'input' || $subdev->{dev} eq 'relay' || $subdev->{dev} eq 'led'); # on,off anstelle von 1,0
readingsBulkUpdateIfChanged($hash,$subdev->{dev}."_".$subdev->{circuit},$value) if defined($value);
Dispatch($hash, $subdev, (%addvals ? \%addvals : undef)) if index(AttrVal($hash->{NAME}, 'logicalDev', 'relay,input,led,ao,temp,,unit_register'), $subdev->{dev}) != -1;
delete $subdev->{value};
readingsBulkUpdateIfChanged($hash,".".$subdev->{dev}."_".$subdev->{circuit},toJSON($subdev),0);
Log3 ($hash, 4, "$hash->{TYPE} ($hash->{NAME}) ".$subdev->{dev}."_".$subdev->{circuit} .": ". toJSON($subdev));
}
}
@circuits = sort @circuits;
my $last;
for my $entry (@circuits) {
Log3 ($hash, 1, "$hash->{TYPE} ($hash->{NAME}) global_id $entry used twice this may cause malfuntions")
if defined $last && $entry eq $last;
$last = $entry;
}
readingsBulkUpdate($hash,"state",(ref $result eq 'HASH' ? $result->{status} : "success") );
readingsEndUpdate($hash,1);
Neuron_ReadingstoSets($hash);
#################################################################
};
if ($@) {
Log3 ($hash, 1, "$hash->{TYPE} ($hash->{NAME}) ParseAll Error: $@");
readingsSingleUpdate($hash,"state","JSON decode error",1);
}
}
return $data;
}
sub Neuron_ParseWsResponse($$){
my ($hash, $data)=@_;
my $name = $hash->{NAME};
my $result;
eval {
$result = JSON->new->utf8(1)->decode($data);
};
if ($@) {
Log3 ($hash, 3, "$hash->{TYPE} ($hash->{NAME}): error decoding response $@\nData:\n$data");
} else {
#my ($subdevs) = $result->{data};
readingsBeginUpdate($hash);
if (ref $result eq 'ARRAY') { #[{"circuit": "1_01", "value": 0, ...}]
foreach (@{$result}){
Neuron_DecodeWsJSON($hash,$_);
}
} elsif (ref $result eq 'HASH') { #{"circuit": "1_01", "value": 0, ...}
Neuron_DecodeWsJSON($hash,$result);
}
readingsEndUpdate($hash,1);
}
return undef
}
sub Neuron_DecodeWsJSON($$){
my ($hash, $dev)=@_;
eval {
readingsBulkUpdate($hash,$dev->{dev}."_".$dev->{circuit},$dev->{value});
Dispatch($hash, $dev, undef) if index(AttrVal($hash->{NAME}, 'logicalDev', 'relay,input,led,ao,temp,unit_register') , $dev->{dev}) != -1;
};
if ($@) {
Log3 ($hash, 3, "$hash->{TYPE} ($hash->{NAME}): error decoding JSON $@\nData:\n$dev");
}
return undef
}
sub Neuron_ReadingstoSets($){
my ($hash)=@_;
my $sets;
my @gets;
foreach (keys %{$hash->{READINGS}}) {
if (substr($_,0,3) eq 'led') {
$sets .= " " if $sets;
$sets .= $_ .":off,on";
} elsif ( substr($_,0,5) eq 'relay') {
$sets .= " " if $sets;
$sets .= $_ .":off,on";
} elsif (substr($_,0,2) eq 'ao') {
$sets .= " " if $sets;
$sets .= $_ .":slider,0,0.1,10";
}
unless (substr($_,0,1) eq '.') {
push (@gets,$_);
}
@gets = sort @gets;
}
$hash->{HELPER}{SETS} = $sets;
$hash->{HELPER}{GETS} = join (',',@gets);
}
#######################################
# Socket Fuctions
#######################################
sub Neuron_Open($) {
my $hash = shift;
my $name = $hash->{NAME};
my $host = $hash->{HOST};
my $port = $hash->{PORT};
my $timeout = 0.1;
Log3 $name, 4, "$hash->{TYPE} ($name) - Establishing socket connection";
######### 1
DevIo_CloseDev($hash) if(DevIo_IsOpen($hash));
DevIo_OpenDev($hash, 0, "Neuron_wsHandshake");
#DevIo_OpenDev($hash, 0, "Neuron_wsHandshake", "Neuron_Callback");
######### 2
# return if( $hash->{CD} );
# my $socket = new IO::Socket::INET ( PeerHost => $host,
# PeerPort => $port,
# Proto => 'tcp',
# Timeout => $timeout
# )
# or return Log3 $name, 4, "$hash->{TYPE} ($name) Couldn't connect to $host:$port"; # open Socket
# $hash->{FD} = $socket->fileno();
# $hash->{CD} = $socket; # sysread / close won't work on fileno
# $selectlist{$name} = $hash;
#########
# Log3 $name, 4, "$hash->{TYPE} ($name) - Socket Connected";
# readingsSingleUpdate($hash,'state','ws_opened',1);
# Neuron_wsHandshake($hash);
}
sub Neuron_Ready($) {
my ($hash) = @_;
return DevIo_OpenDev($hash, 1, "Neuron_wsHandshake") if ( $hash->{STATE} eq "disconnected" );
}
sub Neuron_Close($) {
my $hash = shift;
my $name = $hash->{NAME};
delete $hash->{HELPER}{WESOCKETS};
delete $hash->{HELPER}{wsKey};
######### 1
DevIo_CloseDev($hash);
######### 2
# return if( !$hash->{CD} );
# close($hash->{CD}) if($hash->{CD});
# delete($hash->{FD});
# delete($hash->{CD});
# delete($selectlist{$name});
#########
# readingsSingleUpdate($hash,'state','ws_disconnected',1);
# Log3 $name, 4, "$hash->{TYPE} ($name) - Socket Disconnected";
}
sub Neuron_Write($@) {
my ($hash,$string) = @_;
my $name = $hash->{NAME};
Log3 $name, 4, "$hash->{TYPE} ($name) - WriteFn called:\n$string";
######### 1
DevIo_SimpleWrite($hash, $string, 0);
######### 2
# return Log3 $name, 4, "$hash->{TYPE} ($name) - socket not connected" unless($hash->{CD});
# syswrite($hash->{CD}, $string);
#########
return undef;
}
sub Neuron_Read($) {
my $hash = shift;
my $name = $hash->{NAME};
my $buf;
Log3 $name, 5, "$hash->{TYPE} ($name) - ReadFn started";
########### 1
$buf = DevIo_SimpleRead($hash);
########### 2
# my $len = sysread($hash->{CD},$buf,10240);
# if( !defined($len) or !$len ) {
# Neuron_Close($hash);
# return;
# }
###########
return Log3 $name, 3, "$hash->{TYPE} ($name) - no data received"
unless( defined $buf);
if ($hash->{HELPER}{WESOCKETS}) {
# Fehlerhafte Botschaftsteile abschneiden?
#$buf =~ /(.{2,4}\[\{.*"glob_dev_id": .+\}\])/;
#$buf = $1;
Neuron_wsDecode($hash,$buf);
} elsif( $buf =~ /HTTP\/1.1 101 Switching Protocols/ ) {
Log3 $name, 4, "$hash->{TYPE} ($name) - received HTTP data string, start response processing:\n$buf";
Neuron_wsCheckHandshake($hash,$buf);
} else {
Log3 $name, 1, "$hash->{TYPE} ($name) - corrupted data found:\n$buf";
}
}
sub Neuron_Callback($) {
my ($hash, $error) = @_;
my $name = $hash->{NAME};
Log3 $name, 5, "$hash->{TYPE} ($name) - error while connecting: $error";
return undef;
}
#######################################
# Websocket Functions
#######################################
sub Neuron_wsHandshake($) {
my $hash = shift;
my $name = $hash->{NAME};
my $host = $hash->{HOST};
#my $path = $hash->{PATH};
my $path = "/ws";
my $wsKey = encode_base64(gettimeofday());
my $wsHandshakeCmd = "";
$wsHandshakeCmd .= "GET $path HTTP/1.1\r\n";
$wsHandshakeCmd .= "Host: $host\r\n";
$wsHandshakeCmd .= "User-Agent: FHEM\r\n";
$wsHandshakeCmd .= "Upgrade: websocket\r\n";
$wsHandshakeCmd .= "Connection: Upgrade\r\n";
$wsHandshakeCmd .= "Sec-WebSocket-Version: 13\r\n";
$wsHandshakeCmd .= "Sec-WebSocket-Key: " . $wsKey . "\r\n";
Log3 $name, 4, "$hash->{TYPE} ($name) - Starting Websocket Handshake";
Neuron_Write($hash,$wsHandshakeCmd);
$hash->{HELPER}{wsKey} = $wsKey;
# Log3 $name, 4, "$hash->{TYPE} Websocket ($name) - start WS hearbeat timer";
# Neuron_HbTimer($hash);
return undef;
}
sub Neuron_wsCheckHandshake($$) {
my ($hash,$response) = @_;
my $name = $hash->{NAME};
# header in Hash wandeln
my %header = ();
foreach my $line (split("\r\n", $response)) {
my ($key,$value) = split( ": ", $line );
next if( !$value );
$value =~ s/^ //;
Log3 $name, 4, "$hash->{TYPE} ($name) - headertohash |$key|$value|";
$header{lc($key)} = $value;
}
# check handshake
if( defined($header{'sec-websocket-accept'})) {
my $keyAccept = $header{'sec-websocket-accept'};
Log3 $name, 5, "$hash->{TYPE} ($name) - keyAccept: $keyAccept";
my $wsKey = $hash->{HELPER}{wsKey};
my $expectedResponse = trim(encode_base64(pack('H*', sha1_hex(trim($wsKey)."258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))));
if ($keyAccept eq $expectedResponse) {
Log3 $name, 4, "$hash->{TYPE} ($name) - Successful WS connection to $hash->{HOST}";
readingsSingleUpdate($hash,'state','ws_connected',1);
$hash->{HELPER}{WESOCKETS} = '1';
InternalTimer(gettimeofday() + (5), 'Neuron_wsHertbeat', $hash, 0) if AttrVal($hash->{NAME}, 'wsFilter', '');
#Neuron_wsSetFilter($hash) if AttrVal($hash->{NAME}, 'wsFilter', '');
} else {
Neuron_Close($hash);
Log3 $name, 3, "$hash->{TYPE} ($name) - ERROR: Unsucessfull WS connection to $hash->{HOST}";
readingsSingleUpdate($hash,'state','ws_handshake-error',1);
}
}
return undef;
}
sub Neuron_wsSetFilter($;$) {
my ($hash,$val) = @_;
if ($hash->{HELPER}{wsKey} && DevIo_IsOpen($hash)) {
my $wsFilter = $val || AttrVal($hash->{NAME}, 'wsFilter', 'all');
my $filter = '{"cmd":"filter","devices":["'. join( '","', split(',', $wsFilter ) ) .'"]}';
my $string = Neuron_wsEncode($filter);
Neuron_Write($hash,$string);
}
}
sub Neuron_wsHertbeat($) {
my ($hash) = @_;
if (DevIo_IsOpen($hash)) {
Neuron_wsSetFilter($hash);
InternalTimer(gettimeofday() + (5 * 30), 'Neuron_wsHertbeat', $hash, 0)
}
}
# 0 1 2 3
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
# +-+-+-+-+-------+-+-------------+-------------------------------+
# |F|R|R|R| opcode|M| Payload len | Extended payload length |
# |I|S|S|S| (4) |A| (7) | (16/64) |
# |N|V|V|V| |S| | (if payload len==126/127) |
# | |1|2|3| |K| | |
# +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
# | Extended payload length continued, if payload len == 127 |
# + - - - - - - - - - - - - - - - +-------------------------------+
# | |Masking-key, if MASK set to 1 |
# +-------------------------------+-------------------------------+
## | Masking-key (continued) | Payload Data |
# +-------------------------------- - - - - - - - - - - - - - - - +
# : Payload Data continued ... :
# + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
# | Payload Data continued ... |
# +---------------------------------------------------------------+
# https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
sub Neuron_wsEncode($;$$) {
my ($payload, $type, $masked) = @_;
Log3 undef, 3, "Neuron_wsEncode Payload: " . $payload;
$type //= "text";
$masked //= 1; # Mask If set to 1, a masking key is present in masking-key. 1 for all frames sent from client to server
my $RSV = 0;
my $FIN = 1; # FIN Indicates that this is the final fragment in a message. The first fragment MAY also be the final fragment.
my $MAX_PAYLOAD_SIZE = 65536;
my $wsString ='';
$wsString .= pack 'C', ($opcode{$type} | $RSV | ($FIN ? 128 : 0));
my $len = length($payload);
return "payload to big" if ($len > $MAX_PAYLOAD_SIZE);
if ($len <= 125) {
$len |= 0x80 if $masked;
$wsString .= pack 'C', $len;
} elsif ($len <= 0xffff) {
$wsString .= pack 'C', 126 + ($masked ? 128 : 0);
$wsString .= pack 'n', $len;
} else {
$wsString .= pack 'C', 127 + ($masked ? 128 : 0);
$wsString .= pack 'N', $len >> 32;
$wsString .= pack 'N', ($len & 0xffffffff);
}
if ($masked) {
my $mask = pack 'N', int(rand(2**32));
$wsString .= $mask;
$wsString .= Neuron_wsMasking($payload, $mask);
} else {
$wsString .= $payload;
}
Log3 undef, 3, "Neuron_wsEncode String: " . unpack('H*',$wsString);
return $wsString;
}
sub Neuron_wsDecode($$) {
my ($hash,$wsString) = @_;
Log3 $hash, 5, "Neuron_wsDecode String:\n" . $wsString;
while (length $wsString) {
my $FIN = (ord(substr($wsString,0,1)) & 0b10000000) >> 7;
my $OPCODE = (ord(substr($wsString,0,1)) & 0b00001111);
my $masked = (ord(substr($wsString,1,1)) & 0b10000000) >> 7;
my $len = (ord(substr($wsString,1,1)) & 0b01111111);
my $offset = 2;
if ($len == 126) {
$len = unpack 'n', substr($wsString,$offset,2);
$offset += 2;
} elsif ($len == 127) {
$len = unpack 'q', substr($wsString,$offset,8);
$offset += 8;
}
my $mask;
if($masked) { # Mask auslesen falls Masked Bit gesetzt
$mask = substr($wsString,$offset,4);
$offset += 4;
}
#String kürzer als Längenangabe -> Zwischenspeichern?
if (length($wsString) < $offset + $len) {
Log3 $hash, 4, "Neuron_wsDecode Incomplete:\n" . $wsString;
return;
}
my $payload = substr($wsString, $offset, $len); # Daten aus String extrahieren
if ($masked) { # Daten demaskieren falls maskiert
$payload = Neuron_wsMasking($payload, $mask);
}
Log3 $hash, 5, "Neuron_wsDecode Payload:\n" . $payload;
$wsString = substr($wsString,$offset+$len); # ausgewerteten Stringteil entfernen
if ($FIN) {
if ($OPCODE == $opcode{"text"}) {
Neuron_ParseWsResponse($hash,$payload);
}
}
# Behandlung von Segmentierten Botschaften
# if ($FIN) {
# if (@{$self->{fragments}}) {
# $self->opcode(shift @{$self->{fragments}});
# } else {
# $self->opcode($opcode);
# }
# $payload = join '', @{$self->{fragments}}, $payload;
# $self->{fragments} = [];
# return $payload;
# } else {
# # Remember first fragment opcode
# if (!@{$self->{fragments}}) {
# push @{$self->{fragments}}, $opcode;
# }
# push @{$self->{fragments}}, $payload;
# die "Too many fragments" if @{$self->{fragments}} > $self->{max_fragments_amount};
# }
}
}
sub Neuron_wsMasking($$) {
my ($payload, $mask) = @_;
$mask = $mask x (int(length($payload) / 4) + 1);
$mask = substr($mask, 0, length($payload));
$payload = $payload ^ $mask;
return $payload;
}
1;
=pod
=item device
=item summary Module for EVOK driven devices like UniPi Neuron
=item summary_DE Modul f&uuml; Ger&auml;te auf denen EVOK l&auml;uft z.B. UniPi Neuron.
=begin html
<a name="Neuron"></a>
<h3>Neuron</h3>
<ul>
<a name="Neuron"></a>
Module for EVOK driven devices like UniPi Neuron.<br>
<br>
<a name="NeuronDefine"></a>
<b>Define</b>
<ul>
<code>define <name> Neuron &lt;IP&gt;[:&lt;Port&gt;]</code><br><br>
</ul>
<a name="NeuronSet"></a>
<b>Set</b>
<ul>
<code>set &lt;name&gt; &lt;value&gt; [&lt;args&gt;]</code>
<br><br>
where <code>value</code> can be e.g.:<br>
<ul><li>dev_circuit<br>
outputs only<br>
&lt;args&gt;: on, off for outputs and slider for ao<br>
</li>
<li>clearreadings<br>
delete all readings
</li>
<li>websocket<br>
&lt;arg&gt;: open,close<br>
open, close websocket connection
</li>
<li>postjson<br>
&lt;args&gt;: <code>dev circuit type value</code><br>
send JSON command to subdevice<br>
example: <code>set neuron input 1_01 mode simple</code>
</li>
For details please refer to UniPi Evok documentation.
</ul>
</ul><br>
<a name="NeuronGet"></a>
<b>Get</b>
<ul>
<code>get &lt;name&gt; &lt;value&gt; [&lt;arg&gt;]</code>
<br><br>
where <code>value</code> can be<br>
<ul>
<li>all: refresh all readings</li>
<li>config: returns the configuration from the subdevice &lt;arg&gt;</li>
<li>updt_sets_gets: updates all set and get options</li>
<li>value: returns the state from the subdevice &lt;arg&gt;</li>
</ul>
</ul><br>
<a name="RPI_GPIOAttr"></a>
<b>Attributes</b>
<ul>
<li><a name="connection">connection</a><br>
Set the connection type to the EVOK device<br>
Default: polling<br>
valid values: websockets, polling<br><br>
</li>
<li><a name="poll_interval">poll_interval</a><br>
Set the polling interval in minutes to query all readings (and distribute them to logical devices)<br>
Default: -<br>
valid values: decimal number<br><br>
</li>
<li><a name="wsFilter">wsFilter</a><br>
Filter to limit the list of devices which should send websocket events<br>
Default: all<br>
valid values: all, ai, ao, input, led, relay, wd, temp, unit_register<br><br>
</li>
<li><a name="logicalDev">logicalDev</a><br>
Filter which subdevices should create / communicate with logical device<br>
Default: ao, input, led, relay<br>
valid values: ai, ao, input, led, relay, wd, temp, unit_register<br><br>
</li>
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
</ul>
<br>
</ul>
=end html
=begin html_DE
<a name="Neuron"></a>
<h3>Neuron</h3>
<ul>
<a name="Neuron"></a>
Modul f&uuml;r die Steuerung von Ger&auml;ten auf denen EVOK l&auml;uft z.B. UniPi Neuron.<br>
<br>
<a name="NeuronDefine"></a>
<b>Define</b>
<ul>
<code>define <name> Neuron &lt;IP&gt;[:&lt;Port&gt;]</code><br><br>
</ul>
<a name="NeuronSet"></a>
<b>Set</b>
<ul>
<code>set &lt;name&gt; &lt;value&gt; [&lt;args&gt;]</code>
<br><br>
Werte f&uuml;r <code>value</code>:<br>
<ul><li>dev_circuit<br>
nur f&uuml;r Ausg&auml;nge<br>
&lt;args&gt;: on, off f&uuml;r Ausg&auml;nge und Slider f&uumlr ao<br>
</li>
<li>clearreadings<br>
l&ouml;sche alle Readings
</li>
<li>websocket<br>
&lt;arg&gt;: open,close<br>
Websocket Verbindung &ouml;ffnen, schliessen
</li>
<li>postjson<br>
&lt;args&gt;: <code>dev circuit type value</code><br>
JSON Kommando an entsprechendes Subdevice schicken.<br>
z.B.: <code>set neuron input 1_01 mode simple</code>
</li>
Details dazu sind in der UniPi Evok Dokumentation zu finden.
</ul>
</ul><br>
<a name="NeuronGet"></a>
<b>Get</b>
<ul>
<code>get &lt;name&gt; &lt;value&gt; [&lt;arg&gt;]</code>
<br><br>
Werte f&uuml;r <code>value</code>:<br>
<ul>
<li>all: aktualisiert alle readings</li>
<li>config: gibt die Konfiguration des Subdevices &lt;arg&gt; zur&uuml;ck</li>
<li>updt_sets_gets: Aktualisierung der Set und Get Auswahllisten</li>
<li>value: gibt das Status des Subdevices &lt;arg&gt; zur&uuml;ck</li>
</ul>
</ul><br>
<a name="RPI_GPIOAttr"></a>
<b>Attribute</b>
<ul>
<li><a name="connection">connection</a><br>
Verbindungsart zum EVOK Device<br>
Standard: polling<br>
g&uuml;ltige Werte: websockets, polling<br><br>
</li>
<li><a name="poll_interval">poll_interval</a><br>
Interval in Minuten in dem alle Werte gelesen (und auch an die log. Devices weitergeleitet) werden.<br>
Standard: -<br>
g&uuml;ltige Werte: Dezimalzahl<br><br>
</li>
<li><a name="wsFilter">wsFilter</a>wsFilter<br>
Filter um die liste der Ger&auml;te zu limitieren welche websocket events generieren sollen<br>
Standard: all<br>
g&uuml;ltige Werte: all, ai, ao, input, led, relay, wd, temp, unit_register<br><br>
</li>
<li><a name="logicalDev">logicalDev</a><br>
Filter um Ger&auml;te zu limitieren die logische Devices anlegen und mit ihnen kommunizieren.<br>
Standard: ao, input, led, relay<br>
g&uuml;ltige Werte: ai, ao, input, led, relay, wd, temp, unit_register<br><br>
</li>
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
</ul>
<br>
</ul>
=end html_DE
=cut