diff --git a/fhem/CHANGED b/fhem/CHANGED index 3d5178eeb..75ce58c39 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: new Modules 36_EleroStick.pm and 36_EleroDrive.pm for Elero shutters - change: 55_InfoPanel.pm: support ReadingsVal in ticker #35228 - bugfix: 31_MilightDevice.pm: improved previousState, fixed log errors - bugfix: 30_MilightBridge.pm: fixed ping definition diff --git a/fhem/FHEM/36_EleroDrive.pm b/fhem/FHEM/36_EleroDrive.pm new file mode 100644 index 000000000..b6f9a194c --- /dev/null +++ b/fhem/FHEM/36_EleroDrive.pm @@ -0,0 +1,354 @@ +# $Id: 36_EleroDrive.pm +# +# ToDo-List +# --------- +# [x] attribute -> TopToBottomTime +# [x] Refresh after move command +# [ ] Numeric position reading -> percentOpen (0 ... 100) +# [ ] Move to any position, not only top, bottom, intermediate, ... + + +package main; + +use strict; +use warnings; +use SetExtensions; + +#======================================================================================= +sub EleroDrive_Initialize($) { + my ($hash) = @_; + + $hash->{Match} = ".*"; + $hash->{DefFn} = "EleroDrive_Define"; + $hash->{UndefFn} = "EleroDrive_Undef"; + $hash->{FingerprintFn} = "EleroDrive_Fingerprint"; + $hash->{ParseFn} = "EleroDrive_Parse"; + $hash->{SetFn} = "EleroDrive_Set"; + $hash->{GetFn} = "EleroDrive_Get"; + $hash->{AttrFn} = "EleroDrive_Attr"; + $hash->{AttrList} = "IODev " . + "TopToBottomTime " . + "$readingFnAttributes "; + + $hash->{noAutocreatedFilelog} = 1; +} + + +#======================================================================================= +sub EleroDrive_Define($$) { + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + + return "Usage: define EleroDrive " if(@a < 3); + + my $devName = $a[0]; + my $type = $a[1]; + my $channel = $a[2]; + + $hash->{STATE} = 'Initialized'; + $hash->{NAME} = $devName; + $hash->{TYPE} = $type; + $hash->{channel} = $channel; + + $modules{EleroDrive}{defptr}{$channel} = $hash; + + AssignIoPort($hash); + if(defined($hash->{IODev}->{NAME})) { + Log3 $devName, 4, "$devName: I/O device is " . $hash->{IODev}->{NAME}; + } + else { + Log3 $devName, 1, "$devName: no I/O device"; + } + + return undef; +} + + +#======================================================================================= +sub EleroDrive_Undef($$) { + my ($hash, $arg) = @_; + my $channel = $hash->{channel}; + + RemoveInternalTimer($hash); + delete( $modules{EleroDrive}{defptr}{$channel} ); +} + + +#======================================================================================= +sub EleroDrive_Get($@) { + return undef; +} + + +#======================================================================================= +sub EleroDrive_Set($@) { + my ( $hash, $name, $cmd, @params ) = @_; + + my $channel = $hash->{channel}; + my $iodev = $hash->{IODev}->{NAME}; + + my $commands=("stop:noArg moveDown:noArg moveUp:noArg moveIntermediate:noArg moveTilt:noArg refresh:noArg"); + return $commands if( $cmd eq '?' || $cmd eq ''); + + my $head = 'aa'; + my $msgLength = '05'; + my $msgCmd = '4c'; + my $firstBits = ''; + my $firstChannels = ''; + my $secondBits = ''; + my $secondChannels = ''; + my $checksum = ''; + my $payload = ''; + my $doRefresh = '0'; + + if($cmd eq 'refresh'){ + $payload = '0'; + IOWrite($hash, "refresh", $channel); + } + elsif($cmd eq 'moveDown'){ + $payload = '40'; + $doRefresh = '1'; + } + elsif($cmd eq 'moveUp'){ + $payload = '20'; + $doRefresh = '1'; + } + elsif($cmd eq 'stop'){ + $payload = '10'; + } + elsif($cmd eq 'moveIntermediate'){ + $payload = '44'; + $doRefresh = '1'; + } + elsif($cmd eq 'moveTilt'){ + $payload = '24'; + $doRefresh = '1'; + } + else { + return "Unknown argument $cmd, choose one of $commands"; + } + + if($payload) { + if($channel <= 8){ + $firstChannels = '00'; + $secondChannels = 2**($channel-1); + $secondChannels = sprintf('%02x', $secondChannels); + } + else { + $secondChannels = '00'; + $firstChannels = 2**($channel-1-8); + $firstChannels = sprintf('%02x', $firstChannels); + } + + my $checksumNumber = hex($head) + hex($msgLength) + hex($msgCmd) + hex($firstChannels) + hex($secondChannels) + hex($payload); + my $byteUpperBound = 256; + my $upperBound = $byteUpperBound; + while($checksumNumber > $upperBound){ + $upperBound = $upperBound + $byteUpperBound; + } + $checksumNumber = $upperBound - $checksumNumber; + $checksum = sprintf('%02x', $checksumNumber); + + my $byteMsg = $head.$msgLength.$msgCmd.$firstChannels.$secondChannels.$payload.$checksum; + + ###debugLog($name, "EleroDrive_Set->IOWrite: byteMsg=$byteMsg"); + IOWrite($hash, "send", $byteMsg); + + # Start a one time timer that refreshes the position for this drive + my $refreshDelay = AttrVal($name, "TopToBottomTime", 0); + if($doRefresh && $refreshDelay) { + InternalTimer(gettimeofday() + $refreshDelay + 2, "EleroDrive_OnRefreshTimer", $hash, 0); + } + + } + + return undef; +} + + +#======================================================================================= +sub EleroDrive_Fingerprint($$) { + my ($name, $msg) = @_; + return ("", $msg); +} + + +#======================================================================================= +sub EleroDrive_Parse($$) { + my ($hash, $msg) = @_; + my $name = $hash->{NAME}; + my $buffer = $msg; + + # get the channel + my $firstChannels = substr($buffer,6,2); + my $secondChannels = substr($buffer,8,2); + + my $bytes = $firstChannels.$secondChannels ; + $bytes = hex ($bytes); + + my $channel = 1; + while ($bytes != 1 and $channel <= 15) { + $bytes = $bytes >> 1; + $channel++; + } + + if($channel <= 15) { + # get status + my $statusByte = substr($buffer,10,2); + + my %deviceStati = ('00' => "no_information", + '01' => "top_position", + '02' => "bottom_position", + '03' => "intermediate_position", + '04' => "tilt_position", + '05' => "blocking", + '06' => "overheated", + '07' => "timeout", + '08' => "move_up_started", + '09' => "move_down_started", + '0a' => "moving_up", + '0b' => "moving_down", + '0d' => "stopped_in_undefined_position", + '0e' => "top_tilt_stop", + '0f' => "bottom_intermediate_stop", + '10' => "switching_device_switched_off", + '11' => "switching_device_switched_on" + ); + + my %percentDefinitions = ('00' => 50, + '01' => 100, + '02' => 0, + '03' => 50, + '04' => 50, + '05' => -1, + '06' => -1, + '07' => -1, + '08' => -1, + '09' => -1, + '0a' => -1, + '0b' => -1, + '0d' => 50, + '0e' => 100, + '0f' => 0, + '10' => -1, + '11' => -1 + ); + + my $newstate = $deviceStati{$statusByte}; + my $percentOpen = $percentDefinitions{$statusByte}; + + my $rhash = $modules{EleroDrive}{defptr}{$channel}; + my $rname = $rhash->{NAME}; + + if($modules{EleroDrive}{defptr}{$channel}) { + ###debugLog($name, "$rname -> parsed $msg for channel $channel: $newstate"); + + readingsBeginUpdate($rhash); + readingsBulkUpdate($rhash, "state", $newstate); + readingsBulkUpdate($rhash, "position", $newstate); + if($percentOpen ne -1) { + readingsBulkUpdate($rhash, "percentOpen", $percentOpen); + } + readingsEndUpdate($rhash,1); + + my @list; + push(@list, $rname); + return @list; + } + else { + ###debugLog($name, "$name -> AUTOCREATE " . $hash->{IODev}->{NAME}); + return "UNDEFINED EleroDrive_$channel EleroDrive $channel"; + } + } +} + + +#======================================================================================= +sub EleroDrive_Attr(@) { + +} + +#======================================================================================= +sub EleroDrive_OnRefreshTimer($$) { + my ($hash, @params) = @_; + my $name = $hash->{NAME}; + my $channel = $hash->{channel}; + + IOWrite($hash, "refresh", $channel); + + return undef; +} + + + +1; + +=pod +=begin html + + +

EleroDrive

+ + + +=end html +=cut diff --git a/fhem/FHEM/36_EleroStick.pm b/fhem/FHEM/36_EleroStick.pm new file mode 100644 index 000000000..c66ae9157 --- /dev/null +++ b/fhem/FHEM/36_EleroStick.pm @@ -0,0 +1,484 @@ +# $Id: 36_EleroStick.pm + +# ToDo-List +# --------- +# [ ] Disable the timer when a Drive asks for information to avoid conflicts +# After getting a message in EleroStick_Write we must interupt the timer +# for "ChannelTimeout" seconds +# +# [ ] Perhaps we need a cache for incoming commands that delays the incoming commands +# for "ChannelTimeout" seconds to give the previous command a chance to hear its acknowledge +# +# +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); + +my $clients = ":EleroDrive"; +my %matchList = ("1:EleroDrive" => ".*",); + +# Answer Types +my $easy_confirm = "aa044b"; +my $easy_ack = "aa054d"; + +#======================================================================================= +sub EleroStick_Initialize($) { + my ($hash) = @_; + + require "$attr{global}{modpath}/FHEM/DevIo.pm"; + + $hash->{ReadFn} = "EleroStick_Read"; + $hash->{WriteFn} = "EleroStick_Write"; + $hash->{ReadyFn} = "EleroStick_Ready"; + $hash->{DefFn} = "EleroStick_Define"; + $hash->{UndefFn} = "EleroStick_Undef"; + $hash->{GetFn} = "EleroStick_Get"; + $hash->{SetFn} = "EleroStick_Set"; + $hash->{AttrFn} = "EleroStick_Attr"; + $hash->{FingerprintFn} = "EleroStick_Fingerprint"; + $hash->{ShutdownFn} = "EleroStick_Shutdown"; + $hash->{AttrList} = "Clients " . + "MatchList " . + "ChannelTimeout " . + "Interval " . + "$readingFnAttributes "; + +} + + +#======================================================================================= +sub EleroStick_Fingerprint($$) { +} + +#======================================================================================= +sub EleroStick_SimpleWrite($$) { + my ($hash, $data) = @_; + + DevIo_SimpleWrite($hash, $data, 1); + + if(index($data, "aa054c", 0) == 0) { + readingsSingleUpdate($hash, 'SendType', "easy_send", 1); + } + elsif(index($data, "aa044e", 0) == 0) { + readingsSingleUpdate($hash, 'SendType', "easy_info", 1); + } + elsif(index($data, "aa024a", 0) == 0) { + readingsSingleUpdate($hash, 'SendType', "easy_check", 1); + } + + readingsSingleUpdate($hash, 'SendMsg', $data, 1); +} + + +#======================================================================================= +sub EleroStick_Define($$) { + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + + my $name = $a[0]; + my $type = $a[1]; + my $dev = $a[2]; + + $hash->{USBDev} = $dev; + $hash->{DeviceName} = $dev; + $hash->{NAME} = $name; + $hash->{TYPE} = $type; + $hash->{Clients} = $clients; + $hash->{MatchList} = \%matchList; + + DevIo_OpenDev($hash, 0, undef); + + EleroStick_SendEasyCheck($hash); + + InternalTimer(gettimeofday()+2, "EleroStick_OnTimer", $hash, 0); + + return undef; +} + +#======================================================================================= +sub EleroStick_Undef($$) { + my ( $hash, $arg ) = @_; + DevIo_CloseDev($hash); + RemoveInternalTimer($hash); + return undef; +} + +#======================================================================================= +sub EleroStick_Shutdown($) { + my ($hash) = @_; + $hash->{channels} = ""; + return undef; +} + +#======================================================================================= +sub EleroStick_SendEasyCheck($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + if($hash->{STATE} ne "disconnected") { + my $head = 'aa'; + my $msgLength = '02'; + my $msgCmd = '4a'; + + my $checksumNumber = hex($head) + hex($msgLength) + hex($msgCmd); + my $byteUpperBound = 256; + my $upperBound = $byteUpperBound; + while($checksumNumber > $upperBound){ + $upperBound = $upperBound + $byteUpperBound; + } + $checksumNumber = $upperBound - $checksumNumber; + + my $checksum = sprintf('%02x', $checksumNumber); + + my $byteMsg = $head.$msgLength.$msgCmd.$checksum; + + EleroStick_SimpleWrite($hash, $byteMsg); + + } +} + +#======================================================================================= +sub EleroStick_SendEasyInfo($$) { + my ($hash, $channel) = @_; + my $name = $hash->{NAME}; + + if($hash->{STATE} ne "disconnected") { + my $head = 'aa'; + my $msgLength = '04'; + my $msgCmd = '4e'; + my $firstBits = ''; + my $secondBits = ''; + my $firstChannels = ''; + my $secondChannels = ''; + + if($channel <= 8){ + $firstChannels = '00'; + $secondChannels = 2**($channel-1); + $secondChannels = sprintf('%02x', $secondChannels); + } + else { + $secondChannels = '00'; + $firstChannels = 2**($channel-1-8); + $firstChannels = sprintf('%02x', $firstChannels); + } + + my $checksumNumber = hex($head) + hex($msgLength) + hex($msgCmd) + hex($firstChannels) + hex($secondChannels); + my $byteUpperBound = 256; + my $upperBound = $byteUpperBound; + while($checksumNumber > $upperBound){ + $upperBound = $upperBound + $byteUpperBound; + } + $checksumNumber = $upperBound - $checksumNumber; + my $checksum = sprintf('%02x', $checksumNumber); + + my $byteMsg = $head.$msgLength.$msgCmd.$firstChannels.$secondChannels.$checksum; + + EleroStick_SimpleWrite($hash, $byteMsg); + } + +} + + +#======================================================================================= +sub EleroStick_OnTimer($$) { + my ($hash, @params) = @_; + my $name = $hash->{NAME}; + + my $timerInterval = AttrVal($name, "ChannelTimeout", 5); + + if($hash->{STATE} ne "disconnected") { + if($hash->{channels}) { + my $channels = $hash->{channels}; + + if(index($channels, "x") eq -1) { + # We were at the end of the learned channels or lost our position + my $flc = index($channels, "1"); + if($flc ne -1) { + substr($channels, $flc, 1, "x"); + } + } + + my $now = index($channels, "x"); + if($now ne -1) { + substr($channels, $now, 1, "1"); + ###debugLog($name, "now " . ($now +1)); + EleroStick_SendEasyInfo($hash, $now +1); + + for(my $i = $now +1; $i<15; $i++) { + if(substr($channels, $i, 1) eq "1") { + substr($channels,$i,1,"x"); + last; + } + } + $hash->{channels} = $channels; + } + + # All Channels completed, wait interval seconds + if (index($channels, "x") eq -1) { + $timerInterval = AttrVal($name, "Interval", 60); + } + + } + + } + + InternalTimer(gettimeofday()+$timerInterval, "EleroStick_OnTimer", $hash, 0); + + return undef; +} + + +#======================================================================================= +sub EleroStick_Set($@) { + return undef; +} + + +#======================================================================================= +sub EleroStick_Get($@) { + return undef; +} + + +#======================================================================================= +sub EleroStick_Write($$) { + my ($hash, $cmd, $msg) = @_; + my $name = $hash->{NAME}; + + # Send to the transmitter stick + if($cmd eq 'send'){ + ###debugLog($name, "EleroStick cmd=send msg=$msg"); + EleroStick_SimpleWrite($hash, $msg); + } + + # Request status for a channel + elsif ($cmd eq 'refresh') { + ###debugLog($name, "EleroStick cmd=refresh msg=$msg"); + EleroStick_SendEasyInfo($hash, $msg); + } + +} + + +#======================================================================================= +sub EleroStick_Read($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + # read from serial device + my $buf = DevIo_SimpleRead($hash); + return "" if ( !defined($buf) ); + + # convert to hex string to make parsing with regex easier + my $answer = unpack ('H*', $buf); + + if(index($answer, 'aa', 0) == 0){ + # New Byte String + $hash->{buffer} = $answer; + } + else{ + # Append to Byte String + $hash->{buffer} .= $answer; + } + + #todo: hier gibts manchmal Fehlermeldung $strLen nicht definiert + my $strLen = substr($hash->{buffer},3-1,2); + $strLen = hex($strLen); + my $calLen = ($strLen * 2) + 4; + + if($calLen == length($hash->{buffer})){ + # Die Länge der Nachricht entspricht der Vorgabe im Header + + readingsSingleUpdate($hash,'AnswerMsg', $hash->{buffer},1); + + if(index($hash->{buffer}, $easy_confirm, 0) == 0) { + $hash->{lastAnswerType} = "easy_confirm"; + + my $cc = substr($hash->{buffer},6,4); + my $firstChannels = substr($cc,0,2); + my $secondChannels = substr($cc,2,2); + my $bytes = $firstChannels.$secondChannels ; + $bytes = hex ($bytes); + my $dummy=""; + my $learndChannelFound = 0; + for (my $i=0; $i < 15; $i++) { + if($bytes & 1 << $i) { + if(!$learndChannelFound) { + $dummy = $dummy . "x"; + $learndChannelFound = 1; + } + else { + $dummy = $dummy . "1"; + } + } + else { + $dummy = $dummy . "0"; + } + } + + $hash->{channels} = $dummy; + } + elsif(index($hash->{buffer}, $easy_ack, 0) == 0) { + $hash->{lastAnswerType} = "easy_ack"; + my $buffer = $hash->{buffer}; + Dispatch($hash, $buffer, ""); + } + + readingsSingleUpdate($hash, 'AnswerType', $hash->{lastAnswerType}, 1); + Log3 $name, 4, "Current buffer content: " . $hash->{buffer}." Name ". $hash->{NAME}; + } + else { + # Wait for the rest of the data + Log3 $name, 5, "Current buffer is not long enough "; + } +} + + +#======================================================================================= +sub EleroStick_Ready($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $openResult = DevIo_OpenDev($hash, 1, undef); + + if($hash->{STATE} eq "disconnected") { + $hash->{channels} = ""; + } + else { + EleroStick_SendEasyCheck($hash); + ###debugLog($name, "EleroStick_Ready -> SendEasyInfo"); + } + + return $openResult if($hash->{STATE} eq "disconnected"); + + # This is relevant for windows/USB only + my $po = $hash->{USBDev}; + my ( $BlockingFlags, $InBytes, $OutBytes, $ErrorFlags ) = $po->status; + + return ( $InBytes > 0 ); +} + + +#======================================================================================= +sub EleroStick_Attr(@) { + my ($cmd, $name, $aName, $aVal) = @_; + my $hash = $defs{$name}; + + if($aName eq "Clients") { + $hash->{Clients} = $aVal; + $hash->{Clients} = $clients if( !$hash->{Clients}); + } + + elsif($aName eq "MatchList") { + my $match_list; + if($cmd eq "set") { + $match_list = eval $aVal; + if( $@ ) { + Log3 $name, 2, $name .": $aVal: ". $@; + } + } + + if(ref($match_list) eq 'HASH') { + $hash->{MatchList} = $match_list; + } + else { + $hash->{MatchList} = \%matchList; + } + + } + + return undef; +} + + + + + +#======================================================================================= +1; + +=pod +=begin html + + +

EleroStick

+ + +=end html +=cut