diff --git a/fhem/FHEM/70_CanOverEthernet.pm b/fhem/FHEM/70_CanOverEthernet.pm new file mode 100644 index 000000000..8668cafca --- /dev/null +++ b/fhem/FHEM/70_CanOverEthernet.pm @@ -0,0 +1,217 @@ +############################################################################## +# +# 70_CanOverEthernet.pm +# +# This file is part of Fhem. +# +# Fhem is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# Fhem is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fhem. If not, see . +# +############################################################################## +# +# CanOverEthernet (c) Martin Gutenbrunner / https://github.com/delmar43/FHEM +# +# This module is designed to work as a physical device in connection with 71_COE_Node +# as a logical device. +# +# Discussed in FHEM Forum: https://forum.fhem.de/index.php/topic,96170.0.html +# +# $Id$ +# +############################################################################## +package main; + +use strict; +use warnings; +use IO::Socket; +use DevIo; + +sub CanOverEthernet_Initialize($) { + my ($hash) = @_; + +# require "$attr{global}{modpath}/FHEM/DevIo.pm"; + + $hash->{GetFn} = "CanOverEthernet_Get"; + $hash->{SetFn} = "CanOverEthernet_Set"; + $hash->{DefFn} = "CanOverEthernet_Define"; + $hash->{UndefFn} = "CanOverEthernet_Undef"; + $hash->{ReadFn} = "CanOverEthernet_Read"; + + $hash->{AttrList} = $readingFnAttributes; + $hash->{MatchList} = { "1:COE_Node" => "^.*" }; + $hash->{Clients} = "COE_Node"; + + Log3 '', 3, "CanOverEthernet - Initialize done ..."; +} + +sub CanOverEthernet_Define($$) { + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + + my $name = $a[0]; + my $module = $a[1]; + + if(@a < 2 || @a > 2) { + my $msg = "CanOverEthernet ($name) - Wrong syntax: define CanOverEthernet"; + Log3 undef, 1, $msg; + return $msg; + } + + DevIo_CloseDev($hash); + + $hash->{NAME} = $name; + + Log3 $name, 3, "CanOverEthernet ($name) - Define done ... module=$module"; + + my $portno = 5441; + my $conn = IO::Socket::INET->new(Proto=>"udp",LocalPort=>$portno); + + $hash->{FD} = $conn->fileno(); + $hash->{CD} = $conn; # sysread / close won't work on fileno + $selectlist{$name} = $hash; + + Log3 $name, 3, "CanOverEthernet ($name) - Awaiting UDP connections on port $portno\n"; + + readingsSingleUpdate($hash, 'state', 'defined', 1); + + return undef; +} + +sub CanOverEthernet_Undef($$) { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + DevIo_CloseDev($hash); + + return undef; +} + +sub CanOverEthernet_Read($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $buf; + my $data; + + $hash->{STATE} = 'Last: '.gmtime(); + $hash->{CD}->recv($buf, 16); + $data = unpack('H*', $buf); + Log3 $name, 5, "CanOverEthernet ($name) - Client said $data"; + + Dispatch($hash, $buf); + +} + +sub CanOverEthernet_Get ($@) { + my ( $hash, $param ) = @_; + + my $name = $hash->{NAME}; + Log3 $name, 5, "CanOverEthernet ($name) - Get done ..."; + return undef; +} + +sub CanOverEthernet_Set ($@) +{ + my ( $hash, $name, $cmd, $args ) = @_; + + if ( 'sendData' eq $cmd ) { + CanOverEthernet_parseSendDataCommand( $hash, $name, $args ); + + } + + Log3 $name, 5, "CanOverEthernet ($name) - Set done ..."; + return 'sendData'; +} + +sub CanOverEthernet_parseSendDataCommand { + my ( $hash, $name, @args ) = @_; + + # args: Target-IP Target-Node Index=Value + + my $targetIp = $args[0]; + my $targetNode = $args[1]; + + my $socket = new IO::Socket::INET ( + PeerAddr=>$targetIp, #PeerAddr von $sock ist eingegebener Paramenter $ipaddr + PeerPort=>5441, #PeerPort von $sock ist eingegebener Paramenter $port + Proto=>'udp' #Transportprotokoll: UDP + ); + + if ($socket) { + + my $out = pack('CCSsend($out, 16); + $socket->close(); + Log3 $name, 4, "CanOverEthernet ($name) - sendData done."; + + } else { + Log3 $name, 0, "CanOverEthernet ($name) - sendData failed to create network socket"; + return; + + } + +} + +1; + +=pod +=item [device] +=item summary CanOverEthernet receives COE UDP broadcasts +=item summary_DE CanOverEthernet empfängt CoE UDP broadcasts + +=begin html + + +

CanOverEthernet

+ + + Define +
    + define <name> CanOverEthernet +

    + Defines a CanOverEthernet device. FHEM will start listening to UDP broadcast + on port 5441. +
    + Example: +
      + define coe CanOverEthernet +
    + Actual readings for the incoming data will be written to COE_Node devices, which + are created on-the-fly. +
+ +=end html + +=begin html_DE + + +

CanOverEthernet

+ + + Define +
    + define <name> CanOverEthernet +

    + Erstellt ein CanOverEthernet device. FHEM empfängt auf Port 5441 UDP broadcast. +
    + Beispiel: +
      + define coe CanOverEthernet +
    + Die eingehenden Daten werden als readings in eigenen COE_Node devices gespeichert. + Diese devices werden automatisch angelegt, sobald Daten dafür empfangen werden. +
+ +=end html_DE + +=cut diff --git a/fhem/FHEM/71_COE_Node.pm b/fhem/FHEM/71_COE_Node.pm new file mode 100644 index 000000000..21fa29c33 --- /dev/null +++ b/fhem/FHEM/71_COE_Node.pm @@ -0,0 +1,293 @@ +############################################################################## +# +# 71_COE_Node.pm +# +# This file is part of Fhem. +# +# Fhem is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# Fhem is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fhem. If not, see . +# +############################################################################## +# +# COE_Node (c) Martin Gutenbrunner / https://github.com/delmar43/FHEM +# +# This module is designed to work as a logical device in connection with +# 70_CanOverEthernet as a physical device. +# +# Discussed in FHEM Forum: https://forum.fhem.de/index.php/topic,96170.0.html +# +# $Id$ +# +############################################################################## + +package main; + +use strict; +use warnings; + +sub COE_Node_Initialize { + my ($hash) = @_; + + $hash->{DefFn} = "COE_Node_Define"; + $hash->{ParseFn} = "COE_Node_Parse"; + $hash->{UndefFn} = "COE_Node_Undef"; + $hash->{GetFn} = "COE_Node_Get"; + $hash->{SetFn} = "COE_Node_Set"; + + $hash->{AttrList} = "readingsConfig " . $readingFnAttributes; + $hash->{Match} = "^.*"; + + return undef; +} + +sub COE_Node_Define { + my ( $hash, $def ) = @_; + + my @a = split( "[ \t][ \t]*", $def ); + + my $name = $a[0]; + my $module = $a[1]; + my $canNodeId = $a[2]; + + if(@a < 3 || @a > 3) { + my $msg = "COE_Node ($name) - Wrong syntax: define COE_Node "; + Log3 $name, 1, $msg; + return $msg; + } + + $hash->{NAME} = $name; + + AssignIoPort($hash); + + my $ioDevName = $hash->{IODev}{NAME}; + my $logDevAddress = $ioDevName.'_'.$canNodeId; + + Log3 $name, 5, "COE_Node ($name) - Define: Logical device address: $logDevAddress"; + $modules{COE_Node}{defptr}{$logDevAddress} = $hash; + + Log3 $name, 3, "COE_Node ($name) - Define done ... module=$module, canNodeId=$canNodeId"; + + $hash->{helper}{CAN_NODE_ID} = $canNodeId; + readingsSingleUpdate($hash, 'state', 'defined', 1); + + return undef; +} + +sub COE_Node_Parse { + my ( $io_hash, $buf) = @_; + my $ioDevName = $io_hash->{NAME}; + + my ( $canNodeId, $canNodePartId ) = unpack 'C C', $buf; + my $bytes = substr $buf, 2; + my $logDevAddress = $ioDevName.'_'.$canNodeId; + + # wenn bereits eine Gerätedefinition existiert (via Definition Pointer aus Define-Funktion) + if(my $hash = $modules{COE_Node}{defptr}{$logDevAddress}) { + COE_Node_HandleData($hash, $canNodeId, $canNodePartId, $bytes); + + return $hash->{NAME}; + + } else { + + # Keine Gerätedefinition verfügbar + # Daher Vorschlag define-Befehl: + Log3 $ioDevName, 5, "COE_Node-Parse ($ioDevName) - No definition for $logDevAddress. Suggesting autocreate for canNodeId=$canNodeId"; + + my $ioName = $io_hash->{NAME}; + return "UNDEFINED COE_Node_".$ioDevName."_".$canNodeId." COE_Node $canNodeId"; + } +} + +sub COE_Node_HandleData { + my ( $hash, $canNodeId, $canNodePartId, $bytes ) = @_; + my $name = $hash->{NAME}; + + my $readings = AttrVal($name, 'readingsConfig', undef); + if (! defined $readings) { + Log3 $name, 0, "COE_Node ($name) - No config found. Please set readingsConfig accordingly."; + return undef; + } + + Log3 $name, 4, "COE_Node ($name) - Config found: $readings"; + + # incoming data: 05011700f3000000000001010000 + # extract readings from config, so we know, how to assign each value to a reading + # readings are separated by space + # format: index=name + # example + # 1=T.Solar 2=T.Solar_RL + $hash->{helper}{mapping} = (); + my @readingsArray = split / /, $readings; + foreach my $readingsEntry (@readingsArray) { + Log3 $name, 5, "COE_Node ($name) - $readingsEntry"; + + my @entry = split /=/, $readingsEntry; + $hash->{helper}{mapping}{$entry[0]} = makeReadingName($entry[1]); + } + + if ($canNodeId != $hash->{helper}{CAN_NODE_ID}) { + Log3 $name, 0, "COE_Node ($name) - defined nodeId $hash->{canNodeId} != message-nodeId $canNodeId. Skipping message."; + return undef; + } + + readingsBeginUpdate($hash); + if ( $canNodePartId > 0 ) { + COE_Node_HandleAnalogValues($hash, $canNodePartId, $bytes); + } else { + COE_Node_HandleDigitalValues($hash, $canNodePartId, $bytes); +# Log3 $name, 3, "COE_Node ($name) - [$canNodeId][$canNodePartId][] digital value. skipping for now"; + } + readingsEndUpdate($hash, 1); + +} + +sub COE_Node_HandleAnalogValues { + my ( $hash, $canNodePartId, $bytes ) = @_; + + my @valuesAndTypes = unpack 's s s s C C C C', $bytes; + + my @values = @valuesAndTypes[0..3]; + my @types = @valuesAndTypes[4..7]; + my $canNodeId = $hash->{helper}{CAN_NODE_ID}; + my $name = $hash->{NAME}; + + #iterate through data entries. 4 entries max per incoming UDP packet + for (my $i=0; $i < 4; $i++) { + my $outputId = ($i+($canNodePartId-1)*4 +1); + my $entryId = $outputId; + my $existingConfig = exists $hash->{helper}{mapping}{$entryId}; + my $value = $values[$i]; + my $type = $types[$i]; + + if ($existingConfig) { + + if ($type == 1) { + $value = (substr $value, 0, (length $value)-1) . "." . (substr $value, -1); + } elsif ($type == 13) { + $value = (substr $value, 0, (length $value)-2) . "." . (substr $value, -2); + } + + if ( COE_Node_BeginsWith($value, '.') ) { + $value = "0$value"; + } + + my $reading = $hash->{helper}{mapping}{$entryId}; + readingsBulkUpdateIfChanged( $hash, $reading, $value ); + + Log3 $name, 3, "COE_Node ($name) - [$canNodeId][$canNodePartId][$outputId][$entryId][type=$type][value=$value] configured: $reading"; + } else { + Log3 $name, 3, "COE_Node ($name) - [$canNodeId][$canNodePartId][$outputId][$entryId][type=$type][value=$value] $entryId not configured. Skipping."; + } + } +} + +sub COE_Node_HandleDigitalValues { + my ( $hash, $canNodePartId, $bytes ) = @_; + my $name = $hash->{NAME}; + my $canNodeId = $hash->{helper}{CAN_NODE_ID}; + + my $values = unpack 'b*', $bytes; + my @bits = split //, $values; + + for (my $i=0; $i < 16; $i++) { + my $reading = $hash->{helper}{mapping}{$i+1}; + readingsBulkUpdateIfChanged( $hash, $reading, $bits[$i] ); + Log3 $name, 3, "COE_Node ($name) - [$canNodeId][$canNodePartId][".($i+1)."] = $bits[$i]"; + } +} + +sub COE_Node_BeginsWith { + return substr($_[0], 0, length($_[1])) eq $_[1]; +} + +sub COE_Node_Undef { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + return undef; +} + +sub COE_Node_Get { + return undef; +} + +sub COE_Node_Set { + return undef; +} + +1; + +=pod +=item device +=item summary Single CanOverEthernet node which is created automatically by CanOverEthernet +=item summary_DE Ein einzelner CanOverEthernet Node. Wird automatisch erstellt. + +=begin html + + +

COE_Node

+ + + Define +
    + define <name> COE_Node <CAN-Node ID> +

    + Defines a CanOverEthernet node. FHEM will automatically create these. +
    + Example: +
      + define COE_Node_coe_2 COE_Node 2 +
    + Assigment of readings to incoming values is done in the attribue 'readingsConfig'. +
+

+ + + Attributes +

+
    +
  • readingsConfig {index=reading-name}
    This maps received values to readings. eg 1=Flowrate_Solar 2=T.Solar_Backflow
  • +
+ +=end html + +=begin html_DE + + +

COE_Node

+ + + Define +
    + define <name> COE_Node <CAN-Node ID> +

    + Repräsentiert einen einzelnen CanOverEthernet Node. Wird normalerweise automatisch erstellt. +
    + Beispiel: +
      + define COE_Node_coe_2 COE_Node 2 +
    + Die eintreffenden Werte müssen noch im Attribut 'readingsConfig' einem Reading zugewiesen werden. +
+

+ + + Attributes +

+
    +
  • readingsConfig {index=reading-name}
    Ordnet Werte einem Reading zu. zB 1=Durchfluss_Solar 2=T.Solar_Rücklauf
  • +
+ +=end html_DE + +=cut