2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-01-31 12:49:34 +00:00

98_Modbus: enhance documentation for online help and other smaller changes

git-svn-id: https://svn.fhem.de/fhem/trunk@25963 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
StefanStrobel 2022-04-14 16:52:16 +00:00
parent 89cac37fb3
commit b945244611
2 changed files with 516 additions and 253 deletions

View File

@ -23,7 +23,7 @@
#
# ToDo / Ideas
# limit combine?!! (Max 7d / 125 Register read bzw. 7b write), bei coils read max 7d0, bei write 7b0
# LastError Reading per logical device with error code and affected reading
# verify that nextOpenDelay is integer and >= 1
# set active results in error when tcp is already open
# enforce nextOpenDelay even if slave immediately closes after open https://forum.fhem.de/index.php/topic,75638.570.html
@ -43,12 +43,6 @@
# at modify from tcp to serial iodev hash key and DeviceName key are kept and wrong
# min / max checking as slave when we get write fcodes
#
# document serverTimeout, slave attributes, passive mode, reconnect,
#
# option to close a tcp connection after the response has been received and only open it
# for the next request (connection handling in processRequestQueue instead of only readyfn
#
# put new connection in a special room (even hidden does not work reliably)
# conflicting definitions of attrs for expr etc. when slave uses them
# to write and then to read and send response
# test requesting fc 15 multiple coils
@ -219,7 +213,8 @@ BEGIN { # functions / variables needed from package main
makeReadingName
goodReadingName
DoTrigger
asyncOutput
Log3
RemoveInternalTimer
InternalTimer
@ -256,7 +251,7 @@ BEGIN { # functions / variables needed from package main
init_done
));
# function to be visible im package main as Modbus_Name
# function to be visible in package main as Modbus_Name
GP_Export( qw(
Initialize
));
@ -272,7 +267,7 @@ BEGIN { # functions / variables needed from package main
};
my $Module_Version = '4.4.02 - 31.3.2021';
my $Module_Version = '4.4.04 - 17.7.2021';
my $PhysAttrs = join (' ',
'queueDelay',
@ -303,6 +298,8 @@ my $LogAttrs = join (' ',
'nonPrioritizedSet:0,1',
'nonPrioritizedGet:0,1',
'sortUpdate:0,1',
'cacheUpdateHash:0,1',
'cacheParseInfo:0,1',
'propagateVerbose:0,1',
'connectionsRoom',
'serverIdExpr',
@ -318,6 +315,8 @@ my $ObjAttrs = join (' ',
'obj-[cdih][0-9]+-max',
'obj-[cdih][0-9]+-hint',
'obj-[cdih][0-9]+-map',
'obj-[cdih][0-9]+-mapDefault',
'obj-[cdih][0-9]+-rmapDefault',
'obj-[cdih][0-9]+-set',
'obj-[cdih][0-9]+-setexpr',
'obj-[cdih][0-9]+-textArg',
@ -335,7 +334,10 @@ my $ObjAttrs = join (' ',
'obj-[cdih][0-9]+-allowWrite',
'obj-[cdih][0-9]+-group',
'obj-[cdih][0-9]+-poll',
'obj-[cdih][0-9]+-polldelay');
'obj-[cdih][0-9]+-polldelay',
'obj-[cdih][0-9]+-overrideFCread',
'obj-[cdih][0-9]+-overrideFCwrite'
);
my $DevAttrs = join (' ',
'dev-([cdih]-)?read',
@ -362,7 +364,9 @@ my $DevAttrs = join (' ',
'dev-([cdih]-)?defShowGet',
'dev-([cdih]-)?defAllowWrite',
'dev-([cdih]-)?defPoll',
'dev-([cdih]-)?defPolldelay',
'dev-h-brokenFC3',
'dev-d-brokenFC2',
'dev-c-brokenFC5',
'dev-type-[A-Za-z0-9_]+-unpack',
@ -458,7 +462,8 @@ my %attrDefaults = (
'min' => { default => ''},
'poll' => { devDefault => 'defPoll',
default => 0},
'polldelay' => { default => '0.5'},
'polldelay' => { devDefault => 'defPolldelay',
default => '0.5'},
'reading' => {},
'revRegs' => { devDefault => 'defRevRegs'},
'set' => { devDefault => 'defSet'},
@ -466,10 +471,16 @@ my %attrDefaults = (
'showGet' => { devDefault => 'defShowGet'},
'textArg' => {},
'type' => { default => '***NoTypeInfo***'},
'mapDefault' => { default => undef},
'rmapDefault' => { default => undef},
'unpack' => { devDefault => 'defUnpack',
default => 'n'},
);
my $updateCache; # hash ref to cache getUpdateHash after combine
my $parseInfoCache; # hash ref to cache obj parsing info
###########################################################
# _initialize for the physical io device,
# exported as Modbus_Initialize
@ -534,14 +545,19 @@ sub DefineFn {
return "wrong syntax: define <name> $type [tty-devicename|none]" if (!$dev);
if ($dev =~ /@[\d]+/ || $dev =~ /[Nn]one/) {
$ioHash->{SerialConn} = 1;
delete $ioHash->{TCPConn};
} else {
$ioHash->{TCPConn} = 1;
$dev .= ':502' if ($dev !~ /.*:[0-9]/); # add default port if no port specified
delete $ioHash->{SerialConn};
}
$ioHash->{DeviceName} = $dev; # needed by DevIo to get Device, Port, Speed etc.
$ioHash->{IODev} = $ioHash; # point back to self to make getIOHash easier
$ioHash->{SerialConn} = 1;
$ioHash->{NOTIFYDEV} = 'global'; # NotifyFn nur aufrufen wenn global events (INITIALIZED)
# todo: check if tcp or serial to allow sharing of a tcp connection iodev for multiple devices
# e.g. to a gateway
DoClose($ioHash, 1); # close, set Expect, clear Buffer, but don't set state to disconnected
Log3 $name, 3, "$name: defined as $dev";
return; # open is done later from NOTIFY
@ -749,6 +765,7 @@ sub AttrLDFn {
my $aVal = shift; # attribute value
my $hash = $defs{$name}; # reference to the Fhem device hash
#Log3 $name, 5, "$name: attr $aName " . ($aVal // 'undef') . " $cmd";
if ($cmd eq 'set') {
if ($aName =~ /expr/) { # validate all Expressions
return "Invalid Expression $aVal"
@ -804,7 +821,7 @@ sub AttrLDFn {
return "attribute $aName is only valid for physical Modbus devices or Modbus TCP - please use this attribute for your physical IO device" . ($hash->{IODev}{NAME} ? ' ' . $hash->{IODev}{NAME} : "");
}
}
elsif ($aName =~ /(obj-[cdih])[0-9]+-reading/) {
elsif ($aName =~ /^(obj-[cdih])[0-9]+-reading/) {
return "unsupported character in reading name $aName ".
"(not A-Za-z/\\d_\\.-)" if(!goodReadingName($aName));
}
@ -813,14 +830,14 @@ sub AttrLDFn {
Log3 $name, 3, "$name: attr $aName is only valid Modbus TCP slaves (=servers)";
return "attribute $aName is only valid for Modbus TCP slaves (=servers)";
}
TcpServer_SetSSL($hash); # todo: does this work? is tcp connection open yet? does it have to be?
TcpServer_SetSSL($hash); # check libs and set flag
if($hash->{CD}) {
my $ret = IO::Socket::SSL->start_SSL($hash->{CD});
Log3 $name, 3, "$hash->{NAME} start_SSL: $ret" if($ret);
}
}
if ($aName =~ /(obj-[cdih])(0+([0-9]+))-/) {
if ($aName =~ /^(obj-[cdih])(0+([0-9]+))-/) {
# leading zero in obj-Attr detected
if (length($2) > 5) {
my $new = $1 . substr("00000", 0, 5 - length ($3)) . $3;
@ -851,6 +868,11 @@ sub AttrLDFn {
}
}
}
if ($aName =~ /(^obj-)|(^dev-)/) {
$updateCache = undef; # cached update hash needs to be recalculated when obj / dev info changes
$parseInfoCache = undef; # cached ObjInfo and DevInfo results
}
$hash->{'.updateSetGet'} = 1;
#Log3 $name, 5, "$name: attr change set updateGetSetList to 1";
@ -1012,15 +1034,17 @@ sub FormatSetVal {
my $unpack = ObjInfo($hash, $objCombi, 'unpack');
my $len = ObjInfo($hash, $objCombi, 'len');
my $type = substr($objCombi, 0, 1);
my $fCode = GetFC($hash, {TYPE => $type, LEN => $len, OPERATION => 'write'});
my $adr = substr($objCombi, 1);
my $fCode = GetFC($hash, {TYPE => $type, ADR => $adr, LEN => $len, OPERATION => 'write'});
my $rawVal = $setVal;
# 1. Schritt: Map prüfen
# 1. step: use reverse map if defined, return error if no match
$rawVal = MapConvert ($hash, {map => ObjInfo($hash, $objCombi, 'map'),
default => ObjInfo($hash, $objCombi, 'rmapDefault'), # default for rmapDefault is undef
val => $rawVal, reverse => 1, undefIfNoMatch => 1});
return (undef, "set value $setVal did not match defined map") if (!defined($rawVal));
# 2. Schritt: falls definiert Min- und Max-Werte prüfen
# 2. step: check min / max if defined
if (!CheckRange($hash, {val => $rawVal, min => ObjInfo($hash, $objCombi, 'min'), max => ObjInfo($hash, $objCombi, 'max')} ) ) {
return (undef, "value $rawVal is not within defined min/max range");
}
@ -1029,21 +1053,21 @@ sub FormatSetVal {
return (undef, "Set Value $rawVal is not numeric and textArg not specified");
}
# 3. Schritt: Konvertiere mit setexpr falls definiert
# 3. step: convert using setexpr if defined
$rawVal = EvalExpr($hash, {expr => ObjInfo($hash, $objCombi, 'setexpr'), val => $rawVal});
# 4. Schritt: Pack value
# 4. step: pack value
my $packedVal;
if ($fCode == 5) { # special treatment when writing one coil
if ($type eq 'c' && $fCode == 5) { # special treatment when writing one coil (unless fc5 comes from overriding another type)
my $oneCode = uc DevInfo($hash, 'c', 'brokenFC5', 'FF00');
$packedVal = pack ('H4', ($rawVal ? $oneCode : '0000'));
Log3 $name, 5, "$name: set packed coil to hex " . unpack ('H*', $packedVal);
}
else { # other function code
else { # other type or function code
$packedVal = pack ($unpack, $rawVal);
Log3 $name, 5, "$name: set packed hex " . unpack ('H*', $rawVal) . " with $unpack to hex " . unpack ('H*', $packedVal);
}
# 5. Schritt: RevRegs / SwapRegs if needed
# 5. step: RevRegs / SwapRegs if needed
$packedVal = ReverseWordOrder($hash, $packedVal, $len) if (ObjInfo($hash, $objCombi, 'revRegs'));
$packedVal = SwapByteOrder($hash, $packedVal, $len) if (ObjInfo($hash, $objCombi, 'bswapRegs'));
return ($packedVal, undef);
@ -1094,7 +1118,7 @@ sub SetLDFn {
my $adr = substr($objCombi, 1);
my $len = ObjInfo($hash, $objCombi, 'len');
#my $fCode = DevInfo($hash, $type, 'write', $defaultFCode{$type}{write});
my $fCode = GetFC($hash, {TYPE => $type, LEN => $len, OPERATION => 'write'});
my $fCode = GetFC($hash, {TYPE => $type, ADR => $adr, LEN => $len, OPERATION => 'write'});
my $ioHash = GetIOHash($hash); # ioHash has been checked in GetSetChecks above already
DoRequest($hash, {TYPE => $type, ADR => $adr, LEN => $len, OPERATION => 'write', VALUES => $packedVal, FORCE => !$async, DBGINFO => "set $setName"});
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0}); # call processRequestQueue at next possibility (others waiting?)
@ -1562,7 +1586,7 @@ sub DoOpen {
Log3 $name, 5, "$name: open called from $caller, device is defined with none" if ($caller ne 'Ready');
SetStates($hash, 'opened');
}
elsif (!$hash->{TCPConn} && $hash->{TYPE} ne 'Modbus') {
elsif (!$hash->{TCPConn} && $hash->{TYPE} ne 'Modbus') { # only open physical devices or TCP
Log3 $name, 3, "$name: open called from $caller for logical device - this should not happen";
return;
}
@ -1584,7 +1608,7 @@ sub DoOpen {
SetStates($hash, 'opened');
}
}
else {
else { # normal case, physical device or TCP
my $timeOt = AttrVal($name, 'openTimeout', 3);
my $delay2 = AttrVal($name, 'nextOpenDelay2', 1);
my $nextOp = $hash->{NEXT_OPEN} // 0;
@ -1707,9 +1731,9 @@ sub DoClose {
SetStates($hash, 'disconnected') if (!$noState);
ResetExpect($hash);
DropBuffer($hash);
Profiler($hash, 'Idle'); # set category to book following time, can be Delay, Fhem, Idle, Read, Send or Wait
Profiler($hash, 'Idle'); # set category to book following time, can be Delay, Fhem, Idle, Read, Send or Wait
StopQueueTimer($hash, {silent => 1});
RemoveInternalTimer ("timeout:$name");
RemoveInternalTimer ("timeout:$name"); # remove ResponseTimeout timer when connection is closed
delete $hash->{nextTimeout};
delete $hash->{QUEUE};
return;
@ -1853,7 +1877,7 @@ sub ReadFn {
return if(!defined($buf));
}
HandleGaps ($hash); # check timing / frameGap and remove old buffer if necessary
HandleGaps($hash); # check timing / frameGap and remove old buffer if necessary
$hash->{READ}{BUFFER} .= $buf; # now add new data to buffer
$hash->{REMEMBER}{lrecv} = $now; # rember time for physical side
Log3 $name, 5, "$name: readFn buffer: " . ShowBuffer($hash);
@ -1937,7 +1961,7 @@ sub ReadAnswer {
# nextTimeout is set when a request is sent. This can be the last getUpdate or the get/set
$hash->{nextTimeout} = $now + $timeout if (!$hash->{nextTimeout}); # just to be sure, should not happen.
RemoveInternalTimer ("timeout:$name"); # remove timer, timeout is handled in here now
RemoveInternalTimer ("timeout:$name"); # remove potential existing ResponseTimeout timer, timeout is handled in here now
Profiler($hash, 'Read');
READLOOP:
@ -2094,8 +2118,6 @@ sub ParseFrameStart {
($expectId ? " protocol $proto expecting id $expectId" : '');
use bytes;
if ($proto eq 'RTU') {
# Skip for RTU only works when expectId is passed (parsing Modbus responses from a known Id)
# todo: expectId could be a list of all ids of logical devices defined for this io dev
$frameString = SkipGarbageCheck($hash, ($expectId ? pack('C', $expectId) : undef)); # pass undef if no $expectId
return if ($frameString !~ /(..)(.*)(..)/s); # (id fCode) (data) (crc), return if incomplete. fc17 has no data ...
($id, $fCode) = unpack ('CC', $1);
@ -2197,7 +2219,11 @@ sub HandleResponse {
return if (!ParseResponse($hash, $response, $masterHash)); # frame not complete - continue reading
$hash->{RESPONSE} = $response; # save in receiving io hash for later parsing of response??
if ($request && !$frame->{ERROR}) { # only proceed if we know the request - otherwise fall through and finish parsing
delete $hash->{nextTimeout}; # at least we didn't have a timeout now. Remove it to allow new requests while parsing
delete $hash->{TIMEOUTS}; # clear timeout counter
delete $hash->{RETRY}; # retry counter (if retry after timeout is wanted)
if ($request && !$frame->{ERROR}) { # only parse / relay if we know the request and no error (AddFrameError) - otherwise fall through and finish parsing
Profiler($hash, 'Fhem');
if ($response->{ERRCODE}) { # valid error message response
my $errCode = $errCodes{$response->{ERRCODE}};
@ -2226,12 +2252,9 @@ sub HandleResponse {
Statistics($hash, 'Timeouts', 0); # damit bei Bedarf das Reading gesetzt wird
ResetExpect($hash); # for master back to 'idle', otherwise back to 'request'
Profiler($hash, 'Idle');
delete $hash->{nextTimeout};
delete $hash->{TIMEOUTS};
delete $hash->{RETRY};
delete $hash->{REQUEST};
delete $hash->{REQUEST};
delete $hash->{RESPONSE};
RemoveInternalTimer ("timeout:$name");
RemoveInternalTimer ("timeout:$name"); # remove ResponseTimeout timer now that Response has arrived
StartQueueTimer($hash, \&Modbus::ProcessRequestQueue, {delay => 0}); # set timer to call processRequestQueue asap
return 1; # error or not, parsing is done.
}
@ -2266,6 +2289,11 @@ sub ParseResponse {
return if ($dataLength) < 1;
my ($len, $values) = unpack ('Ca*', $data); # length of values data and values from frame
$values = substr($values, 0, $len) if (length($values) > $len);
if ($fCode == 2 && $masterHash && DevInfo($masterHash, 'd', 'brokenFC2', '') eq 'doepke'
&& length($values) > 1) {
Log3 $name, 5, "$name: ParseResponse uses fix for doepke's broken fcode 2";
$values = substr($values, 1, 1);
}
$response->{VALUES} = $values;
$response->{TYPE} = ($fCode == 1 ? 'c' : 'd'); # coils or discrete inputs
$frame->{PDULEXP} = $len + 2; # 1 Byte fCode + 1 Byte len + len of expected values
@ -2519,14 +2547,15 @@ sub SplitDataString {
}
use bytes;
my ($reading, $unpack, $objLen, $expr);
my ($reading, $unpack, $objLen, $byteLen, $expr);
OBJLOOP:
while (length($dataStr) > 0) { # parse every field / object passed in $transPtr structure
my $objCombi = $type . $startAdr;
$reading = ObjInfo($hash, $objCombi, 'reading'); # '' if nothing specified
if ($type =~ '[cd]') { # coils or digital inputs
$unpack = 'a'; # for coils just take the next byte with 0/1 from the string.
$objLen = 1; # to be used in continue block (go to next coil/input in unpacked bit string)
$unpack = 'a'; # for coils just take the next byte with 0/1 from the string.
$objLen = 1; # to be used in continue block (go to next coil/input in unpacked bit string)
$byteLen = 1; # just take one byte from $dataStr
}
else { # holding / input register
if ($op =~ /^scan/) { # special handling / presentation if scanning
@ -2539,6 +2568,7 @@ sub SplitDataString {
$objLen = ObjInfo($hash, $objCombi, 'len'); # default to 1 (1 Reg / 2 Bytes) with global attrDefaults
$unpack = ObjInfo($hash, $objCombi, 'unpack');
}
$byteLen = $objLen * 2; # one register is two bytes from $dataStr
}
if (!$reading) { # no parse information -> skip to next object
Log3 $name, 5, "$name: SplitDataString has no information about handling $objCombi";
@ -2551,7 +2581,7 @@ sub SplitDataString {
$obj{unpack} = $unpack;
$obj{adr} = $startAdr;
$obj{len} = $objLen;
$obj{data} = substr($dataStr, 0, $objLen * 2);
$obj{data} = substr($dataStr, 0, $byteLen);
$obj{group} = ObjInfo($hash, $objCombi, 'group');
push @objList, \%obj;
}
@ -2563,7 +2593,7 @@ sub SplitDataString {
}
else {
$startAdr += $objLen;
$dataStr = (length($dataStr) > ($objLen*2) ? substr($dataStr, $objLen * 2) : '');
$dataStr = (length($dataStr) > ($byteLen) ? substr($dataStr, $byteLen) : '');
}
#Log3 $name, 5, "$name: SplitDataString moves to next object, skip $objLen to $type$startAdr" if ($dataStr);
}
@ -2571,9 +2601,31 @@ sub SplitDataString {
}
sub CreateParseInfoCache {
my $hash = shift;
my $objCombi = shift;
my $name = $hash->{NAME};
Log3 $name, 5, "$name: CreateParseInfoCache called";
$parseInfoCache->{$objCombi} =
{ 'revRegs' => ObjInfo($hash, $objCombi, 'revRegs'),
'bswapRegs' => ObjInfo($hash, $objCombi, 'bswapRegs'),
'decode' => ObjInfo($hash, $objCombi, 'decode'),
'encode' => ObjInfo($hash, $objCombi, 'encode'),
'ignoreExpr' => ObjInfo($hash, $objCombi, 'ignoreExpr'),
'expr' => ObjInfo($hash, $objCombi, 'expr'),
'map' => ObjInfo($hash, $objCombi, 'map'),
'mapDefault' => ObjInfo($hash, $objCombi, 'mapDefault'),
'rmapDefault' => ObjInfo($hash, $objCombi, 'rmapDefault'),
'format' => ObjInfo($hash, $objCombi, 'format'),
};
return;
}
#######################################################
# create readings from a hash containing all data parts
# with unpack, map, format and so on
# called from ParseDataString which is called from HandleResponse
sub CreateDataObjects {
my $hash = shift;
my $objList = shift;
@ -2592,37 +2644,48 @@ sub CreateDataObjects {
my $objCombi = $obj->{objCombi};
my $objData = $obj->{data};
$objData = ReverseWordOrder($hash, $objData, $obj->{len}) if (ObjInfo($hash, $objCombi, 'revRegs'));
$objData = SwapByteOrder ($hash, $objData, $obj->{len}) if (ObjInfo($hash, $objCombi, 'bswapRegs'));
if ($parseInfoCache->{$objCombi}) {
#Log3 $name, 5, "$name: Cached parse info exists for $objCombi";
CreateParseInfoCache($hash, $objCombi) if (!AttrVal($name, 'cacheParseInfo', 0));
} else {
CreateParseInfoCache($hash, $objCombi);
}
my $pi = $parseInfoCache->{$objCombi};
my @val = unpack ($obj->{unpack}, $objData); # fill @val array in case unpack contains codes for more fields, other elements can be used in expr later.
if (!defined($val[0])) { # undefined value as result of unpack -> skip to next object
$objData = ReverseWordOrder($hash, $objData, $obj->{len}) if ($pi->{'revRegs'});
$objData = SwapByteOrder ($hash, $objData, $obj->{len}) if ($pi->{'bswapRegs'});
# todo: put eval around unpack to catch silly unpack codes that could crash Fhem
my @val = unpack ($obj->{unpack}, $objData); # fill @val array in case unpack contains codes for more fields, other elements can be used in expr later.
if (!defined($val[0])) { # undefined value as result of unpack -> skip to next object
my $logLvl = AttrVal($name, 'timeoutLogLevel', 3);
Log3 $name, $logLvl, "$name: CreateDataObjects unpack of " . unpack ('H*', $objData) . " with $obj->{unpack} for $obj->{reading} resulted in undefined value";
next OBJLOOP;
}
Log3 $name, 5, "$name: CreateDataObjects unpacked " . unpack ('H*', $objData) . " with $obj->{unpack} to " . ReadableArray(\@val);
arrayEncoding($hash, \@val, ObjInfo($hash, $objCombi, 'decode'), ObjInfo($hash, $objCombi, 'encode'));
arrayEncoding($hash, \@val, $pi->{'decode'}, $pi->{'encode'}) if ($pi->{'decode'} || $pi->{'encode'});
my $val = $val[0];
next OBJLOOP if (EvalExpr($hash, # ignore exp results true -> skip to next object
{expr => ObjInfo($hash, $objCombi, 'ignoreExpr'), val => $val,, '@val' => \@val,
next OBJLOOP if ($pi->{'ignoreExpr'} && EvalExpr($hash, # ignore exp results true -> skip to next object
{expr => $pi->{'ignoreExpr'}, val => $val,, '@val' => \@val,
nullIfNoExp => 1, action => "ignoreExpr for $obj->{reading}"}));
if ($transPtr->{OPERATION} && $transPtr->{OPERATION} =~ /^scan/) {
$val = ScanFormat($hash, $val); # interpretations with diferent unpack codes
$val = ScanFormat($hash, $val); # interpretations with diferent unpack codes
}
else {
$val = EvalExpr($hash, {val => $val, expr => ObjInfo($hash, $objCombi, 'expr'), '%val' => \@val});
$val = MapConvert($hash, {val => $val, map => ObjInfo($hash, $objCombi, 'map'), undefIfNoMatch => 0});
$val = FormatVal($hash, {val => $val, format => ObjInfo($hash, $objCombi, 'format')});
$val = EvalExpr($hash, {val => $val, expr => $pi->{'expr'}, '%val' => \@val}) if ($pi->{'expr'});
$val = MapConvert($hash, {val => $val, map => $pi->{'map'},
default => $pi->{'mapDefault'},
undefIfNoMatch => 0}) if ($pi->{'map'});
$val = FormatVal($hash, {val => $val, format => $pi->{'format'}}) if ($pi->{'format'});
}
if ($hash->{MODE} eq 'slave') {
WriteObject($hash, $transPtr, $transPtr->{TYPE}, $obj->{adr}, $val); # do slave write
}
else {
if (!TryCall($hash, 'ModbusReadingsFn', $obj->{reading}, $val)) {
if (!TryCall($hash, 'ModbusReadingsFn', $obj->{reading}, $val)) { # unless a user module defined ModbusReadingsFn
Log3 $name, 4, "$name: CreateDataObjects assigns value $val to $obj->{reading}";
readingsBulkUpdate($hash, $obj->{reading}, $val);
}
@ -3067,16 +3130,19 @@ sub CreateResponse {
# get the correct function code
# called from DoRequest
sub GetFC {
my $hash = shift;
my $request = shift;
my $type = $request->{TYPE};
my $len = $request->{LEN};
my $op = $request->{OPERATION};
my $name = $hash->{NAME}; # name of logical device
my $fcKey = ($op =~ /^scan/ ? 'read' : $op);
my $hash = shift;
my $request = shift;
my $type = $request->{TYPE};
my $objCombi = $request->{TYPE} . $request->{ADR};
my $len = $request->{LEN};
my $op = $request->{OPERATION} // 'read';
my $name = $hash->{NAME}; # name of logical device
my $fcKey = ($op =~ /^scan/ ? 'read' : $op);
#my $defFC = $defaultFCode{$type}{$fcKey};
my $defFC = 3;
# find default function code first
SEARCH:
foreach my $fc (keys %fcMap) {
if ($fcMap{$fc}{type} && $fcMap{$fc}{type} eq $type && exists $fcMap{$fc}{$op} && exists $fcMap{$fc}{default}) {
@ -3084,16 +3150,23 @@ sub GetFC {
last SEARCH;
}
}
$defFC = 16 if ($defFC == 6 && $request->{LEN} > 1);
my $fCode = DevInfo($hash, $type, $fcKey, $defFC);
$defFC = 16 if ($defFC == 6 && $len > 1);
my $fCode = DevInfo($hash, $type, $fcKey, $defFC); # attribute or devInfo Hash to get fc for "read" or "write"
my $override = ObjInfo($hash, $objCombi, 'overrideFC'.$op); # attr to override fc for read / write
$fCode = $override if ($override);
if (!$fCode) {
Log3 $name, 3, "$name: GetFC called from " . FhemCaller() . " did not find fCode for $fcKey type $type";
}
elsif ($fCode == 6 && $request->{LEN} > 1) {
elsif ($fCode == 6 && $len > 1) {
Log3 $name, 3, "$name: GetFC called from " . FhemCaller() . ' tries to use function code 6 to write more than one register. This will not work!';
}
elsif ($fCode !~ /^[0-9]+$/) {
Log3 $name, 3, "$name: GetFC called from " . FhemCaller() . ' get fCode $fCode which is not numeric. This will not work!';
}
else {
#Log3 $name, 5, "$name: GetFC called from " . FhemCaller() . " returns fCode $fCode for $fcKey $objCombi len $len";
}
return $fCode;
}
@ -3359,29 +3432,34 @@ sub ProcessRequestQueue {
}
return if (CheckDelays($ioHash, $maHash, $request)); # might set Profiler to delay
my $pdu = PackRequest($ioHash, $request);
my $frame = PackFrame($ioHash, $reqId, $pdu, $request->{TID});
LogFrame ($ioHash, "ProcessRequestQueue (V$Module_Version) qlen $qlen, sending "
. ShowBuffer($ioHash, $frame) . " via $ioHash->{DeviceName}", 4, $request);
$request->{SENT} = $now;
$request->{FRAME} = $frame; # frame as data string for echo detection
$ioHash->{REQUEST} = $request; # save for later handling incoming response
$ioHash->{EXPECT} = 'response'; # expect to read a response
DropBuffer($ioHash);
Statistics($ioHash, 'Requests');
SendFrame($ioHash, $reqId, $frame, $maHash); # send the request, set Profiler key to 'Send'
Profiler($ioHash, 'Wait'); # wait for response to our request
RemoveInternalTimer ("timeout:$name"); # remove potential existing ResponseTimeout timer - will be set later
my $timeout = DevInfo($maHash, 'timing', 'timeout', ($request->{RELAYHASH} ? 1.5 : 2));
my $toTime = $now+$timeout;
RemoveInternalTimer ("timeout:$name");
InternalTimer($toTime, \&Modbus::ResponseTimeout, "timeout:$name", 0);
$ioHash->{nextTimeout} = $toTime; # to be able to calculate remaining timeout time in ReadAnswer
my $pdu = PackRequest($ioHash, $request);
if ($pdu) {
my $frame = PackFrame($ioHash, $reqId, $pdu, $request->{TID});
LogFrame ($ioHash, "ProcessRequestQueue (V$Module_Version) qlen $qlen, sending "
. ShowBuffer($ioHash, $frame) . " via $ioHash->{DeviceName}", 4, $request);
$request->{SENT} = $now;
$request->{FRAME} = $frame; # frame as data string for echo detection
$ioHash->{REQUEST} = $request; # save for later handling incoming response
$ioHash->{EXPECT} = 'response'; # expect to read a response
shift(@{$queue}); # remove first element from queue
Statistics($ioHash, 'Requests');
SendFrame($ioHash, $reqId, $frame, $maHash); # send the request, set Profiler key to 'Send'
Profiler($ioHash, 'Wait'); # wait for response to our request
my $timeout = DevInfo($maHash, 'timing', 'timeout', ($request->{RELAYHASH} ? 1.5 : 2));
my $toTime = $now+$timeout;
InternalTimer($toTime, \&Modbus::ResponseTimeout, "timeout:$name", 0);
$ioHash->{nextTimeout} = $toTime; # to be able to calculate remaining timeout time in ReadAnswer
} else {
Log3 $name, 3, "ProcessRequestQueue (V$Module_Version) qlen $qlen cant send empty pdu";
}
shift(@{$queue}); # remove first element from queue
readingsSingleUpdate($ioHash, 'QueueLength', ($queue ? scalar(@{$queue}) : 0), 1) if (AttrVal($name, 'enableQueueLengthReading', 0));
StartQueueTimer($ioHash, \&Modbus::ProcessRequestQueue); # schedule next call if there are more items in the queue
return;
@ -3390,13 +3468,12 @@ sub ProcessRequestQueue {
###########################################################
# Pack holding / input register / coil Data for a response,
# only called from createResponse which is only called from HandleRequest
# only called from createResponse which is only called from HandleRequest (slave mode)
# with logical device hash and the response hash
#
# two lengths:
# one (valuesLen) from the response hash LEN (copied from the request length)
# and one (len) from the objInfo for the current object
#
sub PackObj {
my $logHash = shift;
my $response = shift;
@ -3452,10 +3529,12 @@ sub PackObj {
Log3 $name, 4, "$name: PackObj for $objCombi is using reading $rname of device $device with value $val";
}
$val = EvalExpr($logHash, {expr => $expr, val => $val, '$type' => $type, '%startAdr' => $startAdr} );
$val = FormatVal($logHash, {val => $val, format => ObjInfo($logHash, $objCombi, 'format')});
$val = MapConvert($logHash, {map => ObjInfo($logHash, $objCombi, 'map'),
val => $val, reverse => 1, undefIfNoMatch => 1});
$val = EvalExpr($logHash, {expr => $expr, val => $val, '$type' => $type, '%startAdr' => $startAdr} );
$val = FormatVal($logHash, {val => $val, format => ObjInfo($logHash, $objCombi, 'format')});
$val = MapConvert($logHash, {map => ObjInfo($logHash, $objCombi, 'map'), # convert with reverse map
default => ObjInfo($logHash, $objCombi, 'rmapDefault'),
val => $val, reverse => 1, undefIfNoMatch => 1}); # undef if no match and no default
$val = 0 if (!defined($val)); # avoid working with undef when reverse map did not match
$val = decode($decode, $val) if ($decode); # decode
$val = encode($encode, $val) if ($encode); # encode again
@ -3464,12 +3543,12 @@ sub PackObj {
$counter++;
}
else {
my $valLog = (defined ($val) ? "value $val" : "undefined value");
local $SIG{__WARN__} = sub { Log3 $name, 3, "$name: PackObj pack for $objCombi " .
(defined ($val) ? "value $val" : "undefined value") .
" $val with code $unpack created warning: @_"; };
"$valLog with code $unpack created warning: @_"; };
my $dataPart = pack ($unpack, $val); # use unpack code, might create warnings
Log3 $name, 5, "$name: PackObj packed $val with pack code $unpack to " . unpack ('H*', $dataPart);
$dataPart = substr ($dataPart . pack ('x' . $len * 2, undef), 0, $len * 2);
Log3 $name, 5, "$name: PackObj packed $valLog with pack code $unpack to " . unpack ('H*', $dataPart);
$dataPart = substr ($dataPart . pack ('x' . $len * 2, undef), 0, $len * 2); # pad with \0 bytes created by pack
Log3 $name, 5, "$name: PackObj padded / cut object to " . unpack ('H*', $dataPart);
$counter += $len;
Log3 $name, 5, "$name: PackObj revRegs = $revRegs, dplen = " . length($dataPart);
@ -3699,6 +3778,7 @@ sub CreateUpdateHash {
my $devInfo = ($hash->{deviceInfo} ? $hash->{deviceInfo} : $modHash->{deviceInfo});
my $intvl = $hash->{Interval};
my $now = gettimeofday();
my $ignDelay = AttrVal($name, 'cacheUpdateHash', 0);
my @RawObjList;
foreach my $attribute (keys %{$attr{$name}}) { # add all reading attributes to a list unless they are also in parseInfo
@ -3707,7 +3787,7 @@ sub CreateUpdateHash {
}
};
push @RawObjList, keys (%{$parseInfo}); # add all parseInfo readings to the list
Log3 $name, 5, "$name: CreateUpdateList full object list: " . join (' ', sort @RawObjList);
Log3 $name, 5, "$name: CreateUpdateHash full object list: " . join (' ', sort @RawObjList);
my @objList;
my %objHash;
@ -3715,7 +3795,6 @@ sub CreateUpdateHash {
foreach my $objCombi (sort compObjCombi @RawObjList) { # sorted by type+adr
my $reading = ObjInfo($hash, $objCombi, 'reading');
my $poll = ObjInfo($hash, $objCombi, 'poll');
my $delay = ObjInfo($hash, $objCombi, 'polldelay');
my $group = ObjInfo($hash, $objCombi, 'group');
my $len = ObjInfo($hash, $objCombi, 'len');
my $lastRead = $hash->{lastRead}{$objCombi} // 0;
@ -3723,31 +3802,35 @@ sub CreateUpdateHash {
my $adr = substr($objCombi, 1);
my $maxLen = DevInfo($hash, $type, 'combine', 0);
my $objText = "$objCombi len $len $reading";
#Log3 $name, 5, "$name: CreateUpdateList check $objCombi reading $reading, poll = $poll, polldelay = $delay, last = $lastRead";
my $delay = ($ignDelay ? 0 : ObjInfo($hash, $objCombi, 'polldelay')); # ignore Polldelay when caching update hash
$maxLen = 125 if ($maxLen > 125 && $type =~ /[hi]/); # max allowed combine for modbus holding registers or input
$maxLen = 2000 if ($maxLen > 2000 && $type =~ /[cd]/); # max allowed combine for coils / digital inputs
#Log3 $name, 5, "$name: CreateUpdateHash check $objCombi reading $reading, poll = $poll, polldelay = $delay, last = $lastRead";
my $groupNr;
$groupNr = $1 if ($group && $group =~ /(\d+)-(\d+)/);
if ($groupNr) { # handle group
if ($groupNr) { # handle group - objects to be requested together
my $objRef = $grpHash{'g'.$groupNr};
my $span = 0;
if ($objRef) {
$span = $adr - $objRef->{adr} + $len;
if ($objRef->{type} ne $type) {
Log3 $name, 3, "$name: CreateUpdateList found incompatible types in group $groupNr (so far $objRef->{type}, now $type";
Log3 $name, 3, "$name: CreateUpdateHash found incompatible types in group $groupNr (so far $objRef->{type}, now $type";
}
elsif ($objRef->{adr} > $adr) {
Log3 $name, 3, "$name: CreateUpdateList found wrong adr sorting in group $groupNr. Old $objRef->{adr}, new $adr. Please report this bug";
Log3 $name, 3, "$name: CreateUpdateHash found wrong adr sorting in group $groupNr. Old $objRef->{adr}, new $adr. Please report this bug";
}
elsif ($maxLen && $span > $maxLen) {
Log3 $name, 3, "$name: CreateUpdateList found group $groupNr span $span is longer than defined maximum $maxLen";
Log3 $name, 3, "$name: CreateUpdateHash found group $groupNr span $span is longer than defined maximum $maxLen";
}
else { # add to group
$objRef->{len} = $span;
$objRef->{groupInfo} .= ($objRef->{groupInfo} ? ' and ' : '') . $objText;
#Log3 $name, 5, "$name: CreateUpdateList adds $objText to group $groupNr";
#Log3 $name, 5, "$name: CreateUpdateHash adds $objText to group $groupNr";
}
}
else { # new object for group
#Log3 $name, 5, "$name: CreateUpdateList creates new hash for group $groupNr with $objText";
#Log3 $name, 5, "$name: CreateUpdateHash creates new hash for group $groupNr with $objText";
$objRef = {type => $type, adr => $adr, len => $len, reading => $reading,
groupInfo => $objText, group => $group, objCombi => 'g'.$groupNr};
$grpHash{'g'.$groupNr} = $objRef;
@ -3757,23 +3840,23 @@ sub CreateUpdateHash {
if (!$delay || ($delay && $delay ne 'once') || ($delay eq 'once' && !$lastRead)) {
$delay = 0 if ($delay eq 'once' || !$delay);
$delay = $1 * ($intvl ? $intvl : 1) if ($delay =~ /^x([0-9]+)/); # delay as multiplyer if starts with x
if ($now >= $lastRead + $delay) { # this object is due to be requested
if ($now >= $lastRead + $delay) { # this object is due to be requested
if ($groupNr) {
$objHash{'g'.$groupNr} = $grpHash{'g'.$groupNr};
Log3 $name, 5, "$name: CreateUpdateList will request group $groupNr because of $objText";
Log3 $name, 5, "$name: CreateUpdateHash will request group $groupNr because of $objText";
}
else { # no group
else { # no group
$objHash{$objCombi} = {objCombi => $objCombi, type => $type, adr => $adr, reading => $reading, len => $len};
Log3 $name, 5, "$name: CreateUpdateList will request $objText";
Log3 $name, 5, "$name: CreateUpdateHash will request $objText";
}
}
else { # delay not over
if ($groupNr && $objHash{'g'.$groupNr}) { # but part of a group to be requested
Log3 $name, 5, "$name: CreateUpdateList will request $reading because it is part of group $groupNr";
Log3 $name, 5, "$name: CreateUpdateHash will request $reading because it is part of group $groupNr";
}
else { # delay not over and not in a group to be requested
my $passed = $now - $lastRead;
Log3 $name, 5, "$name: CreateUpdateList will skip $reading, delay not over (delay $delay, $passed passed)";
Log3 $name, 5, "$name: CreateUpdateHash will skip $reading, delay not over (delay $delay, $passed passed)";
}
}
}
@ -3802,17 +3885,20 @@ sub CombineUpdateHash {
COMBINELOOP:
foreach my $nextObj (sort compObjTA values %{$objHash}) { # sorting type/adr
$maxLen = DevInfo($hash, $nextObj->{type}, 'combine', 1);
next COMBINELOOP if (!$lastObj); # initial round
next COMBINELOOP if ($maxLen < 2 || !$lastObj); # initial round or no combination wanted
$reason = '';
$lastText = $lastObj->{groupInfo} ? "$lastObj->{objCombi} len $lastObj->{len} ($lastObj->{groupInfo})"
: "$lastObj->{objCombi} len $lastObj->{len} $lastObj->{reading}";
$nextText = $nextObj->{groupInfo} ? "$nextObj->{objCombi} len $nextObj->{len} ($nextObj->{groupInfo})"
: "$nextObj->{objCombi} len $nextObj->{len} $nextObj->{reading}";
$nextSpan = ($nextObj->{adr} + $nextObj->{len}) - $lastObj->{adr}; # combined length
if ($nextObj->{adr} <= $lastObj->{adr}) {
if (GetFC($hash, {TYPE => $nextObj->{type}, ADR => $nextObj->{adr}, LEN => $nextObj->{len}, OPERATION => 'read'}) !=
GetFC($hash, {TYPE => $lastObj->{type}, ADR => $lastObj->{adr}, LEN => $lastObj->{len}, OPERATION => 'read'})) {
$reason = "different function codes";
} elsif ($nextObj->{adr} <= $lastObj->{adr}) {
$reason = 'wrong order defined';
} elsif ($nextObj->{type} ne $lastObj->{type}) {
$reason = 'different types';
} elsif ($nextSpan > $maxLen) {
$reason = "span $nextSpan would be bigger than max $maxLen";
}
@ -3866,8 +3952,16 @@ sub GetUpdate {
my $ioHash = GetIOHash($hash); # only needed for profiling, availability id checked in CheckDisable
Profiler($ioHash, 'Fhem');
my $objHash = CreateUpdateHash($hash);
CombineUpdateHash($hash, $objHash);
my $objHash;
if (!AttrVal($name, 'cacheUpdateHash', 0) || !$updateCache) {
$objHash = CreateUpdateHash($hash);
CombineUpdateHash($hash, $objHash);
$updateCache = $objHash;
}
else {
Log3 $name, 4, "$name: GetUpdate is using cached object list";
$objHash = $updateCache;
}
# now create the requests
foreach my $obj (sort compObjTA values %{$objHash}) { # sorted by type / adr
@ -3877,7 +3971,7 @@ sub GetUpdate {
DBGINFO => "getUpdate for " .
($obj->{combine} ? "combined $obj->{combine}" : "$obj->{reading} len $obj->{len}")});
}
Profiler($ioHash, 'Idle');
Profiler($ioHash, 'Idle');
return;
}
@ -4009,7 +4103,6 @@ sub CheckChecksum {
delete $frame->{CHECKSUMERROR};
if ($proto eq 'RTU') {
# todo: optimize
my $frameLen = $frame->{PDULEXP} + $PDUOverhead{$hash->{PROTOCOL}}; # everything including id to crc
# for RTU Overhead is 3 (id ... 2 Bytes CRC)
my $crcInputLen = ($readLen < $frameLen ? $readLen : $frameLen) - 2; # frame without 2 bytes crc
@ -4474,8 +4567,6 @@ sub RegisterAtIODev {
# or when device is deleted
# see attr, notify or directly from undef
################################################################
# todo: Tests for register / unregister with several modes / protocols
# todo: Tests with relays, rename MasterSlave1 to OpenDelays
sub UnregAtIODev {
my $hash = shift;
my $silent = shift;
@ -4646,7 +4737,7 @@ sub ResetExpect {
########################################
# used for sorting and combine checking
sub compObjCombi ($$) { ## no critic - seems to be required here
sub compObjCombi ($$) { ## no critic - seems to be required here when used for sort
my ($a,$b) = @_;
my $aType = substr($a, 0, 1);
my $aStart = substr($a, 1);
@ -4662,7 +4753,7 @@ sub compObjCombi ($$) { ## no critic - seems to be required
##############################################################################
# used for sorting hashes that contain data objects for reading creation
# compare $obj{$objCombi}{group} group-order values
sub compObjGroups ($$) { ## no critic - seems to be required here
sub compObjGroups ($$) { ## no critic - seems to be required here when used for sort
my ($a, $b) = @_;
my $aGrp = $a->{group} // 0;
my $bGrp = $b->{group} // 0;
@ -4769,13 +4860,13 @@ sub ObjInfo {
my $oName = shift;
my $defName = $attrDefaults{$oName}{devDefault};
my $lastDefault = $attrDefaults{$oName}{default};
$hash = $hash->{CHILDOF} if ($hash->{CHILDOF}); # take info from parent device if TCP server conn (TCP slave)
my $name = $hash->{NAME};
my $modHash = $modules{$hash->{TYPE}};
my $parseInfo = ($hash->{parseInfo} ? $hash->{parseInfo} : $modHash->{parseInfo});
#Log3 $name, 5, "$name: ObjInfo called from " . FhemCaller() . " for $key, object $oName" .
# ($defName ? ", defName $defName" : '') . ($lastDefault ? ", lastDefault $lastDefault" : '');
# ($defName ? ", defName $defName" : '');
my $reading = ObjAttr($hash, $key, 'reading');
if (!defined($reading) && $parseInfo->{$key} && $parseInfo->{$key}{reading}) {
@ -4783,7 +4874,7 @@ sub ObjInfo {
}
if (!defined($reading)) {
#Log3 $name, 5, "$name: ObjInfo could not find a reading name";
return (defined($lastDefault) ? $lastDefault : '');
return (exists($attrDefaults{$oName}{default}) ? $attrDefaults{$oName}{default} : '');
}
#Log3 $name, 5, "$name: ObjInfo now looks at attrs for oName $oName / reading $reading / $key";
@ -4836,7 +4927,8 @@ sub ObjInfo {
return $devInfo->{$type}{$defName}
if (defined($devInfo->{$type}) && defined($devInfo->{$type}{$defName}));
}
return (defined($lastDefault) ? $lastDefault : '');
# todo: the final return expression seems redundant
return (exists($attrDefaults{$oName}{default}) ? $attrDefaults{$oName}{default} : '');
}
@ -4919,13 +5011,15 @@ sub TryCall {
=item summary_DE Basismodul für Geräte mit Modbus-Interface
=begin html
<a name="Modbus"></a>
<a id="Modbus"></a>
<h3>Modbus</h3>
<ul>
Modbus defines a physical modbus interface and functions to be called from other logical modules / devices.
Modbus defines a shared modbus i/o interface and functions to be called from other logical modules / devices.
This low level module takes care of the communication with modbus devices and provides Get, Set and cyclic polling
of Readings as well as formatting and input validation functions.
The logical device modules for individual machines only need to define the supported modbus function codes and objects of the machine with the modbus interface in data structures. These data structures are then used by this low level module to implement Set, Get and automatic updateing of readings in a given interval.
The logical device modules for individual machines only need to define the supported modbus function codes and objects of the machine
with the modbus interface in data structures.
These data structures are then used by this low level module to implement Set, Get and automatic updateing of readings in a given interval.
<br>
This version of the Modbus module supports Modbus RTU and ASCII over serial / RS485 lines as well as Modbus TCP and Modbus RTU or RTU over TCP.
It defines read / write functions for Modbus holding registers, input registers, coils and discrete inputs.
@ -4939,12 +5033,14 @@ sub TryCall {
</ul>
<br>
<a name="ModbusDefine"></a>
<a id="Modbus-define"></a>
<b>Define</b>
<ul>
<code>define &lt;name&gt; Modbus &lt;device&gt; </code>
<br><br>
A define of a physical device based on this module is only necessary if a shared physical device like a RS485 USB adapter is used. In the case of Modbus TCP this module will be used as a library for other modules that define all the data objects and no define of the base module is needed.
A define of a low level io device based on this module is only necessary if a shared device like a RS485 USB adapter is used
or if you need to pass several logical connections through a shared tcp connection e.g. to a gateway.
This module will also be used as a library for other high level modules that define all the data objects.
<br>
Example:<br>
<br>
@ -4958,47 +5054,85 @@ sub TryCall {
</ul>
<br>
<a name="ModbusSet"></a>
<a id="Modbus-set"></a>
<b>Set-Commands</b><br>
<ul>
this low level device module doesn't provide set commands for itself but implements set
for logical device modules that make use of this module. See ModbusAttr for example.
</ul>
<br>
<a name="ModbusGet"></a>
<a id="Modbus-get"></a>
<b>Get-Commands</b><br>
<ul>
this low level device module doesn't provide get commands for itself but implements get
for logical device modules that make use of this module.
</ul>
<br>
<a name="ModbusAttr"></a>
<a id="Modbus-attr"></a>
<b>Attributes</b><br><br>
<ul>
<li><a href="#do_not_notify">do_not_notify</a></li>
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
<li><a href="#do_not_notify">do_not_notify</a>
</li>
<li><a href="#readingFnAttributes">readingFnAttributes</a>
</li>
<br>
<li><b>queueMax</b></li>
<li><a id="Modbus-attr-queueMax">queueMax</a><br>
max length of the queue used for sending requests, defaults to 200.
<li><b>queueDelay</b></li>
</li>
<li><a id="Modbus-attr-queueDelay">queueDelay</a><br>
modify the delay used when sending requests to the device from the internal queue, defaults to 1 second
<li><b>queueTimeout</b></li>
</li>
<li><a id="Modbus-attr-queueTimeout">queueTimeout</a><br>
modify the timeout used to remove old entries in the send queue for requests. By default entries that cound not be sent for more than 20 seconds will be deleted from the queue
<li><b>enableQueueLengthReading</b></li>
if set to 1 the physical device will create a reading with the length of the queue ued internally to send requests.<br>
</li>
<li><a id="Modbus-attr-enableQueueLengthReading">enableQueueLengthReading</a><br>
if set to 1 the physical device will create a reading with the length of the queue used internally to send requests.<br>
<li><b>busDelay</b></li>
</li>
<li><a id="Modbus-attr-busDelay">busDelay</a><br>
defines a delay that is always enforced between the last read from the bus and the next send to the bus for all connected devices
<li><b>clientSwitchDelay</b></li>
</li>
<li><a id="Modbus-attr-clientSwitchDelay">clientSwitchDelay</a><br>
defines a delay that is always enforced between the last read from the bus and the next send to the bus for all connected devices but only if the next send goes to a different device than the last one
<li><b>frameGap</b></li>
</li>
<li><a id="Modbus-attr-frameGap">frameGap</a><br>
defines the time after which the read buffer is discarded if no frame has been received. This defaults to 1.5 seconds.
<li><b>dropQueueDoubles</b></li>
</li>
<li><a id="Modbus-attr-dropQueueDoubles">dropQueueDoubles</a><br>
prevents new request to be queued if the same request is already in the send queue
<li><b>retriesAfterTimeout</b></li>
tbd.
</li>
<li><a id="Modbus-attr-retriesAfterTimeout">retriesAfterTimeout</a><br>
defines how often the module will try to resend a request after a timeout. This defaults to 0
</li>
<li><a id="Modbus-attr-maxTimeoutsToReconnect">maxTimeoutsToReconnect</a><br>
defines that the module will disconnect and reconnect a connection after the given number of successive timeouts
</li>
<li><a id="Modbus-attr-timeoutLogLevel">maxTimeoutstimeoutLogLevelToReconnect</a><br>
defines at which log level timeout messages will be logged. It defaults to 3 and could e.g. be set to 4.
</li>
<li><a id="Modbus-attr-closeAfterResponse">closeAfterResponse</a><br>
if this attribute is set to 1 then the module will only open a connection when it needs to send a request.
After receiving the response the connection is closed unless the send queue contains more requests to be sent.
This might be helpful if modbus TCP slaves have to be queried by multiple masters but the slave is unable to handle more than one connection at a time.
</li>
<li><a id="Modbus-attr-nextOpenDelay">nextOpenDelay</a><br>
delay in seconds that is passed to DevIo. It is enforced between successive calls to DevIo_Open.
</li>
<li><a id="Modbus-attr-nextOpenDelay2">nextOpenDelay2</a><br>
delay in seconds to override nextOpenDelay in DevIo. This delay is enforced inside to Modbus module. Normally this attribute should never be needed.
</li>
<li><a id="Modbus-attr-openTimeout">openTimeout</a><br>
timeout in seconds that is passed to DevIo.
</li>
<li><a id="Modbus-attr-silentReconnect">silentReconnect</a><br>
this attribute controls at what loglevel reconnect messages from devIO will be logged. Without this attribute they will be logged at level 3.
If this attribute is set to 1 then such messages will be logged at level 4.
</li>
<li><b>skipGarbage</b></li>
<li><a id="Modbus-attr-skipGarbage">skipGarbage</a><br>
If the module is used as master or if it is using Modbus ASCII as protocol, then the module will skip bytes received
that can not be the start of correct frames.<br>
For Modbus ASCII it skips bytes until the expected starting byte ":" is seen.
@ -5008,8 +5142,9 @@ sub TryCall {
Or if the last frame was a request, then it skips everything until the modbus id of this request is seen as the start of a response.
Setting this attribuet to 1 might lead to more robustness, however when there are other slaves on the same bus, it might als create trouble when other slaves do not send responses.
<br>
</li>
<li><b>profileInterval</b></li>
<li><a id="Modbus-attr-profileInterval">profileInterval</a><br>
if set to something non zero it is the time period in seconds for which the module will create bus usage statistics.
Please note that this number should be at least twice as big as the interval used for requesting values in logical devices that use this physical device<br>
The bus usage statistics create the following readings:
@ -5030,9 +5165,10 @@ sub TryCall {
number of requests sent
<li><b>Statistics_Timeouts</b></li>
timeouts encountered
</ul>
</li>
</ul>
<br>
</ul>
=end html

View File

@ -61,7 +61,7 @@ sub Initialize {
=begin html
<a name="ModbusAttr"></a>
<a id="ModbusAttr"></a>
<h3>ModbusAttr</h3>
<ul>
ModbusAttr uses the low level Modbus module 98_Modbus.pm to provide a generic Modbus module (as master, slave, relay or passive listener) <br>
@ -83,7 +83,7 @@ sub Initialize {
</ul>
<br>
<a name="ModbusAttrDefine"></a>
<a id="ModbusAttr-define"></a>
<b>Define as Modbus master (=client)</b>
<ul>
<code>
@ -209,7 +209,7 @@ sub Initialize {
<br>
<a name="ModbusAttrConfiguration"></a>
<a id="ModbusAttr-configuration"></a>
<b>Configuration of the module as master or passive listener</b>
<ul>
Data objects (holding registers, input registers, coils or discrete inputs) are defined using attributes.
@ -287,7 +287,7 @@ sub Initialize {
</ul>
<br>
<a name="ModbusAttrDataTypes"></a>
<a id="ModbusAttr-dataTypes"></a>
<b>Handling Data Types</b>
<ul>
The Modbus protocol does not define data types. If the documentation of a device states that for example the current temperature is stored in holding register 102 this leaves room for many interpretations. Not only can the address 102 mean different things (actually decimal 102 or rather 101 if the vendor starts counting at 1 instead of 0 or even 257 or 258 if the vendor used hexadecimal addresses in his documentation ) also the data representation can be many different things. As in every programming language, there are many ways to represent numbers. They can be stored signed or unsigned, they can be integers or floating point numbers, the byte-order can be "big endian" or "small endian", the value can be stored in one holding register or in two holding registers (floating point numbers typically take four bytes which means two holding registers).<br>
@ -332,7 +332,7 @@ sub Initialize {
</ul>
<br>
<a name="ModbusAttrConfigurationSlave"></a>
<a id="ModbusAttr-configurationSlave"></a>
<b>Configuration of the module as Modbus slave (server)</b>
<ul>
Data objects that the module offers to external Modbus masters (holding registers, input registers, coils or discrete inputs) are defined using attributes.
@ -365,7 +365,7 @@ sub Initialize {
</ul>
<br>
<a name="ModbusAttrSet"></a>
<a id="ModbusAttr-set"></a>
<b>Set-Commands for Fhem as Modbus master operation</b>
<ul>
are created based on the attributes defining the data objects.<br>
@ -407,7 +407,7 @@ sub Initialize {
</ul>
</ul>
<br>
<a name="ModbusAttrGet"></a>
<a id="ModbusAttr-get"></a>
<b>Get-Commands for Modbus master operation</b><br>
<ul>
All readings are also available as Get commands. Internally a Get command triggers the corresponding
@ -415,15 +415,18 @@ sub Initialize {
To avoid huge option lists in FHEMWEB, the objects visible as Get in FHEMWEB can be defined by setting an attribute <code>obj-xy-showGet</code> to 1.
</ul>
<br>
<a name="ModbusAttrattr"></a>
<a id="ModbusAttr-attr"></a>
<b>Attributes</b><br><br>
<ul>
<li><a href="#do_not_notify">do_not_notify</a></li>
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
<li><a href="#do_not_notify">do_not_notify</a>
</li>
<li><a href="#readingFnAttributes">readingFnAttributes</a>
</li>
<br>
<li><b>alignTime</b></li>
<li><a id="ModbusAttr-attr-alignTime">alignTime</a><br>
Aligns each periodic read request for the defined interval to this base time. This is typcally something like 00:00 (see the Fhem at command)
<li><b>enableControlSet</b></li>
</li>
<li><a id="ModbusAttr-attr-enableControlSet">enableControlSet</a><br>
enables the built in set commands like interval, stop, start and reread (see above).<br>
Starting with Version 4 of the Modbus module enableControlSet defaults to 1. This attribute can however be used to disable the set commands by setting the attribute to 0<br>
<br>
@ -434,60 +437,90 @@ sub Initialize {
the following list of attributes can be applied to any data object by specifying the objects type and address in the variable part.
For many attributes you can also specify default values per object type (see dev- attributes later) or you can specify an object attribute without type and address
(e.g. obj-len) which then applies as default for all objects:
<li><b>obj-[cdih][1-9][0-9]*-reading</b></li>
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-reading" data-pattern="obj-.*-reading">obj-[cdih][0-9]+-reading</a><br>
define the name of a reading that corresponds to the modbus data object of type c,d,i or h and a decimal address (e.g. obj-h225-reading).<br>
For master or passive operation this reading name will be used to create a reading for the modbus device itself. <br>
For slave operation this can also be specified as deviceName:readingName to refer to the reading of another device inside Fhem whose value can be queried by an external Modbus master with the goven type and address.<br>
<li><b>obj-[cdih][1-9][0-9]*-name</b></li>
defines an optional internal name of this data object (this has no meaning for fhem and serves mainly documentation purposes.<br>
<li><b>obj-[cdih][1-9][0-9]*-set</b></li>
if set to 1 then this data object can be changed with a Fhem set command
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-name" data-pattern="obj-.*-name">obj-[cdih][0-9]+-name</a><br>
defines an optional internal name of the data object of type c,d,i or h and a decimal address (e.g. obj-h225-name).<br>
This has no meaning for fhem and serves mainly documentation purposes.<br>
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-set" data-pattern="obj-.*-set">obj-[cdih][0-9]+-set</a><br>
if set to 1 then this data object (e.g. obj-h225) can be changed with a Fhem set command
which results in a modbus write request sent to the external slave device.<br>
(works only if this device is a modbus master and for holding registers and coils
since discrete inputs and input registers can not be modified by definition).<br>
<li><b>obj-[cdih][1-9][0-9]*-min</b></li>
this defines a lower limit to the value of this data object<br>
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-min" data-pattern="obj-.*-min">obj-[cdih][0-9]+-min</a><br>
this defines a lower limit to the value of this data object (e.g. obj-h225-min).<br>
If in master mode this applies to values written with a Fhem set command to an external slave device and is used for input validation.<br>
If in slave mode this applies to values written by an external master device to Fhem readings.<br>
<li><b>obj-[cdih][1-9][0-9]*-max</b></li>
this defines an upper limit to the value of this data object<br>
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-max" data-pattern="obj-.*-max">obj-[cdih][0-9]+-max</a><br>
this defines an upper limit to the value of this data object (e.g. obj-h225-max)<br>
If in master mode this applies to values written with a Fhem set command to an external slave device and is used for input validation.<br>
If in slave mode this applies to values written by an external master device to Fhem readings.<br>
<li><b>obj-[cdih][1-9][0-9]*-hint</b></li>
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-hint" data-pattern="obj-.*-hint">obj-[cdih][0-9]+-hint</a><br>
this is used in master mode for set options and tells fhemweb what selection to display for the set option (list or slider etc.)<br>
<li><b>obj-[cdih][1-9][0-9]*-expr</b></li>
Example: attr MBTest obj-h225-hint slider,5,1,75
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-expr" data-pattern="obj-.*-expr">obj-[cdih][0-9]+-expr</a><br>
In master mode this defines a perl expression that converts the raw value read from an external slave device into a value that is stored in a Fhem reading.<br>
In slave mode this defines a perl expression that converts the raw value written from an external master device into a value that is stored in a Fhem reading.<br>
Inside the expression you can use $val to get the value or the array @val in case there are several values (e.g. when unpack produces more than one value)<br>
<li><b>obj-[cdih][1-9][0-9]*-setexpr</b></li>
Example: attr MBTest obj-h225-expr $val * 2
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-setexpr" data-pattern="obj-.*-setexpr">obj-[cdih][0-9]+-setexpr</a><br>
In master mode this defines a perl expression that converts the user specified value from the set command
to a raw value that can be sent to the external slave device with a write function code.<br>
In slave mode this defines a perl expression that converts the value of a reading inside Fhem to a raw value that can be sent to the device
as a response to the read function code received from the external master device.<br>
This is typically the inversion of -expr above.<br
This is typically the inversion of -expr above.<br>
Inside the expression you can use $val to get the value or the array @val in case there are several values (e.g. when unpack produces more than one value)<br>
<li><b>obj-[cdih][1-9][0-9]*-allowWrite</b></li>
Example: attr MBTest obj-h225-setexpr $val / 2
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-allowWrite" data-pattern="obj-.*-allowWrite">obj-[cdih][0-9]+-allowWrite</a><br>
this only applies to a Fhem Modbus device in slave mode.
If set to 1 it defines that a reading can be changed with a write function code by an external modbus master.<br>
<li><b>obj-[cdih][1-9][0-9]*-ignoreExpr</b></li>
Example: attr MBTest obj-h333-allowWrite 1
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-ignoreExpr" data-pattern="obj-.*-ignoreExpr">obj-[cdih][0-9]+-ignoreExpr</a><br>
defines a perl expression that returns 1 if a value should be ignored and the existing reading should not be modified<br>
In master mode this applies to values read from an external slave device.<br>
In slave mode this applies to values written to Fhem readings by an external master device.<br>
Inside the expression you can use $val to get the value or the array @val in case there are several values (e.g. when unpack produces more than one value)<br>
<li><b>obj-[cdih][1-9][0-9]*-map</b></li>
Example: attr MBTest obj-h333-ignoreExpr $val > 100
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-map" data-pattern="obj-.*-map">obj-[cdih][0-9]+-map</a><br>
In master mode defines a map to convert raw values read from an external device to more convenient strings that are then stored in Fhem readings
or back (as reversed map) when a value to write has to be converted from the user set value to a raw value that can be written.<br>
In slave mode defines a map to convert raw values received from an external device with a write function code to more convenient strings that are then stored in Fhem readings<br>
or back (as reversed map) when a value to read has to be converted from the Fhem reading value to a raw value that can be sent back as response.<br>
Example: 0:mittig, 1:oberhalb, 2:unterhalb<br>
<li><b>obj-[cdih][1-9][0-9]*-format</b></li>
Example: attr MBTest obj-h225-map 0:mittig, 1:oberhalb, 2:unterhalb
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-mapDefault" data-pattern="obj-.*-mapDefault">obj-[cdih][0-9]+-mapDefault</a><br>
defines a default value to be used with a map (for output manipulation). This value will be returned if there is no match in the map<br>
Example: attr MBTest obj-h225-mapDefault other
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-rmapDefault" data-pattern="obj-.*-rmapDefault">obj-[cdih][0-9]+-rmapDefault</a><br>
defines a default value to be used with a reverse map (e.g. for input validation). This value will be returned if there is no match in the map
Example: attr MBTest obj-h225-rmapDefault 0
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-format" data-pattern="obj-.*-format">obj-[cdih][0-9]+-format</a><br>
In master mode this defines a format string (see Perl sprintf) to format a value read from an external slave device before it is stored in a reading e.g. %.1f <br>
In slave mode this defines a format string to format a value from a Fhem reading before it is sent back in a response to an external master <br>
<li><b>obj-[cdih][1-9][0-9]*-len</b></li>
defines the length of the data object in registers. It defaults to 1. <br>
Example: attr MBTest obj-h225-format %.1f
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-len" data-pattern="obj-.*-len">obj-[cdih][0-9]+-len</a><br>
defines the length of the data object in registers (16 Bits). It defaults to 1. <br>
Some devices store e.g. 32 bit floating point values in two registers. In this case you should set this attribute to two.<br>
This setting is relevant both in master and in slave mode. The lenght has to match the length implied by the unpack code.
<li><b>obj-[cdih][1-9][0-9]*-unpack</b></li>
This setting is relevant both in master and in slave mode. The lenght has to match the length implied by the unpack code. <br>
Example: attr MBTest obj-h225-len 2
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-unpack" data-pattern="obj-.*-unpack">obj-[cdih][0-9]+-unpack</a><br>
defines the pack / unpack code to convert data types.<br>
In master mode it converts the raw data string read from the external slave device to a reading or to convert from a reading to a raw format when a write request is sent to the external slave device.<br>
In slave mode it converts the value of a reading in Fhem to a raw format that can be sent as a response to an external Modbus master or it converts the raw data string read from the external master device to a reading when the master is using a write function code and writing has been allowed.<br>
@ -496,20 +529,31 @@ sub Initialize {
and for a 32 bit big endian float value this would be e.g. "f>". (see the perl documentation of the pack function for more codes and details).<br>
Please note that you also have to set a -len attribute (for this object or for the device) if you specify an unpack code that consumes data from more than one register.<br>
For a 32 bit float e.g. len should be 2.<br>
<li><b>obj-[cdih][1-9][0-9]*-revRegs</b></li>
Example: attr MBTest obj-h225-unpack n
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-revRegs" data-pattern="obj-.*-revRegs">obj-[cdih][0-9]+-revRegs</a><br>
this is only applicable to objects that span several input registers or holding registers. <br>
When they are received from an external device then the order of the registers will be reversed before further interpretation / unpacking of the raw register string. The same happens before data is sent to an external device<br>
<li><b>obj-[cdih][1-9][0-9]*-bswapRegs</b></li>
When they are received from an external device then the order of the registers will be reversed before further interpretation / unpacking
of the raw register string. The same happens before data is sent to an external device<br>
Example: attr MBTest obj-h225-revRegs 1
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-bswapRegs" data-pattern="obj-.*-bswapRegs">obj-[cdih][0-9]+-bswapRegs</a><br>
After registers have been received and before they are sent, the byte order of all 16-bit values are swapped. This changes big-endian to little-endian or vice versa. This functionality is most likely used for reading (ASCII) strings from devices where they are stored as big-endian 16-bit values. <br>
Example: original reading is "324d3130203a57577361657320722020". After applying bswapRegs, the value will be "4d3230313a2057576173736572202020"
which will result in the ASCII string "M201: WWasser ". Should be used with "(a*)" as -unpack value.<br>
<li><b>obj-[cdih][1-9][0-9]*-decode</b></li>
Example: attr MBTest obj-h225-bswapRegs 1
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-decode" data-pattern="obj-.*-decode"></a><br>
defines an encoding to be used in a call to the perl function decode to convert the raw data string received from a device.
This can be used if the device delivers strings in an encoding like cp850 instead of utf8.<br>
<li><b>obj-[cdih][1-9][0-9]*-encode</b></li>
Example: attr MBTest obj-h225-decode cp850
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-encode" data-pattern="obj-.*-encode">obj-[cdih][0-9]+-encode</a><br>
defines an encoding to be used in a call to the perl function encode to convert raw data strings received from a device.
This can be used if the device delivers strings in an encoding like cp850 and after decoding it you want to reencode it to e.g. utf8.<br>
<li><b>obj-[ih][1-9][0-9]*-type</b></li>
Example: attr MBTest obj-h225-encode utf8
</li>
<li><a id="ModbusAttr-attr-obj-[ih][0-9]+-type" data-pattern="obj-.*-type">obj-[ih][0-9]+-type</a><br>
defines that this object has a user defined data type. Data types can be defined using the dev-type- attribues.<br>
If a device with many objects uses for example floating point values that span two swapped registers with the unpack code f>, then instead of specifying the -unpack, -revRegs, -len, -format and other attributes over and over again, you could define a data type with attributes that start with dev-type-VT_R4- and then
use this definition for each object as e.g. obj-h1234-type VT_R4<br>
@ -524,15 +568,21 @@ sub Initialize {
attr WP obj-h1234-type VT_R4
</pre><br>
<li><b>obj-[cdih][1-9][0-9]*-showGet</b></li>
If the Fhem Modbus device is in master mode, every reading can also be requested by a get command. However these get commands are not automatically offered in fhemweb. By specifying this attribute, the get will be visible in fhemweb.<br>
<li><b>obj-[cdih][1-9][0-9]*-poll</b></li>
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-showGet" data-pattern="obj-.*-showGet">obj-[cdih][0-9]+-showGet</a><br>
If the Fhem Modbus device is in master mode, every reading can also be requested by a get command.
However these get commands are not automatically offered in fhemweb. By specifying this attribute, the get will be visible in fhemweb.<br>
Example: attr MBTest obj-h225-showGet 1
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-poll" data-pattern="obj-.*-poll">obj-[cdih][0-9]+-poll</a><br>
If the Fhem Modbus device is in master mode, Fhem automatically creates read requests to the external modbus slave.
If this attribute is set to 1 for an object then this obeject is included in the cyclic update request as specified in the define command for a Modbus master.
If not set, then the object can manually be requested with a get command, but it is not automatically updated each interval.
Note that this setting can also be specified as default for all objects with the dev- atributes described later.<br>
This attribute is ignored in slave mode.<br>
<li><b>obj-[cdih][1-9][0-9]*-polldelay</b></li>
Example: attr MBTest obj-h225-poll 1
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-polldelay" data-pattern="obj-.*-pollDelay">obj-[cdih][0-9]+-polldelay</a><br>
this applies only to master mode. It allows to poll objects at a lower rate than the interval specified in the define command.
You can either specify a time in seconds or number prefixed by "x" which means a multiple of the interval of the define command.<br>
If you specify a normal numer then it is interpreted as minimal time between the last read and another automatic read.<br>
@ -540,7 +590,9 @@ sub Initialize {
Instead the normal interval timer defined by the interval of the define command will check if this reading is due or not yet.
So the effective interval will always be a multiple of the interval of the define.<br>
If this attribute is set to "once" then the object will only be requested once after a restart.<br>
<li><b>obj-[cdih][1-9][0-9]*-group</b></li>
Example: attr MBTest obj-h225-pollDelay x3
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-group" data-pattern="obj-.*-group">obj-[cdih][0-9]+-group</a><br>
Allows control over the way how objects are combined in one request and in which order they are processed when the response comes in.<br>
example:<br>
<pre>
@ -561,120 +613,195 @@ sub Initialize {
this will cause the holding registers 100 and 102 to be read together. When the response is received,
register 102 will be processed first so when register 100 is processed, its value can be multipied with the already updated reading for register 102.<br>
This is helpful for devices where readings need to be computed out of several registers that need to be requested together and where the order of processing is important.
<li><b>dev-([cdih]-)*read</b></li>
</li>
<li><a id="ModbusAttr-attr-obj-[cdih][0-9]+-overrideFCread" data-pattern="obj-.*-overrideFC.*">obj-[cdih][0-9]+-overrideFCread and obj-[cdih][0-9]+-overrideFCwrite</a><br>
allow overwriting a function call number to be used when reading or writing an individual object.<br>
Please do not use this attribute unless you understand the modbus protocol and its function codes.
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?read" data-pattern="dev-.*read">dev-([cdih]-)?read</a><br>
specifies the function code to use for reading this type of object in master mode.
The default is 3 for holding registers, 1 for coils, 2 for discrete inputs and 4 for input registers.<br>
<li><b>dev-([cdih]-)*write</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?write" data-pattern="dev-.*write">dev-([cdih]-)?write</a><br>
specifies the function code (decimal) to use for writing this type of object in master mode.
The default is 6 for holding registers and 5 for coils. Discrete inputs and input registers can not be written by definition.<br>
Some slave devices might need function code 16 for writing holding registers. In this case dev-h-write can be set to 16.
<li><b>dev-([cdih]-)*combine</b></li>
Some slave devices might need function code 16 for writing holding registers. In this case dev-h-write can be set to 16.<br>
Example: attr MBTest dev-h-write 16
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?combine" data-pattern="dev-.*combine">dev-([cdih]-)?combine</a><br>
This applies only to master mode. It defines how many adjacent objects of an external slave device can be read in one request. If not specified, the default is 1<br>
<li><b>dev-([cdih]-)*addressErrCode</b></li>
If this value is too big, some data will not be read.<br>
Example: attr MBTest dev-h-combine 8
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?addressErrCode" data-pattern="dev-.*addressErrCode">dev-([cdih]-)?addressErrCode</a><br>
This applies only if the Fhem Modbus device is in slave mode.
defines which error code to send back to a master that requests an object with an address that is not configured in Fhem.<br>
If nothing is specified, the error code 2 is used. If 0 is specified, then no error is sent back.<br>
<li><b>dev-([cdih]-)*valueErrCode</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?valueErrCode" data-pattern="dev-.*valueErrCode">dev-([cdih]-)?valueErrCode</a><br>
This applies only if the Fhem Modbus device is in slave mode.
It defines which error code to send back to a master that tries to write a value to an object / reading where the value is lower than the specified minimum value or higher than the specified maximum value. (this feature is not implemented yet)<br>
If nothing is specified, the error code 1 is used. If 0 is specified, then no error is sent back.<br>
<li><b>dev-([cdih]-)*notAllowedErrCode</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?notAllowedErrCode" data-pattern="dev-.*notAllowedErrCode">dev-([cdih]-)?notAllowedErrCode</a><br>
This applies only if the Fhem Modbus device is in slave mode.
It defines which error code to send back to a master that tries to write to an object / reading where writing has not been allowed with the .<br>
If nothing is specified, the error code 1 is used. If 0 is specified, then no error is sent back.<br>
<li><b>dev-([cdih]-)*defLen</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defLen" data-pattern="dev-.*defLen">dev-([cdih]-)?defLen</a><br>
defines the default length for this object type. If not specified, the default is 1<br>
<li><b>dev-([cdih]-)*defFormat</b></li>
Example: attr MBTest dev-h-defLen 2
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defFormat" data-pattern="dev-.*defFormat">dev-([cdih]-)?defFormat</a><br>
defines a default format string to use for this object type in a sprintf function on the values read from the device.<br>
<li><b>dev-([cdih]-)*defExpr</b></li>
Example: attr MBTest dev-h-defFormat %.1f
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defExpr" data-pattern="dev-.*defExpr">dev-([cdih]-)?defExpr</a><br>
defines a default Perl expression to use for this object type to convert raw values read. (see obj-...-expr)<br>
<li><b>dev-([cdih]-)*defSetexpr</b></li>
Example: attr MBTest dev-h-defExpr $val / 10
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defSetexpr" data-pattern="dev-.*defSetExpr">dev-([cdih]-)?defSetexpr</a><br>
defines a default Perl expression to use like -setexpr (see obj-...-setexpr)<br>
<li><b>dev-[cdih][1-9][0-9]*-defAllowWrite</b></li>
Example: attr MBTest dev-h-defSetexpr $val * 10
</li>
<li><a id="ModbusAttr-attr-dev-[cdih][0-9]+-defAllowWrite" data-pattern="dev-.*defAllowWrite">dev-[cdih][0-9]+-defAllowWrite</a><br>
this only applies to a Fhem Modbus device in slave mode. <br>
If set to 1 it defines that readings can be changed with a write function code by an external modbus master.<br>
<li><b>dev-([cdih]-)*defIgnoreExpr</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defIgnoreExpr" data-pattern="dev-.*defIgnoreExpr">dev-([cdih]-)?defIgnoreExpr</a><br>
defines a default Perl expression to decide when values should be ignored.<br>
<li><b>dev-([cdih]-)*defUnpack</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defUnpack" data-pattern="dev-.*-defUnpack">dev-([cdih]-)?defUnpack</a><br>
defines the default unpack code for this object type. <br>
<li><b>dev-([cdih]-)*defRevRegs</b></li>
Example: attr MBTest dev-h-defUnpack f>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defRevRegs" data-pattern="dev-.*-defRevRegs">dev-([cdih]-)?defRevRegs</a><br>
defines that the order of registers for objects that span several registers will be reversed before
further interpretation / unpacking of the raw register string<br>
<li><b>dev-([cdih]-)*defBswapRegs</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defBswapRegs" data-pattern="dev-.*defBswapRegs">dev-([cdih]-)?defBswapRegs</a><br>
per device default for swapping the bytes in Registers (see obj-bswapRegs above)<br>
<li><b>dev-([cdih]-)*defDecode</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defDecode" data-pattern="dev-.*defDecode">dev-([cdih]-)?defDecode</a><br>
defines a default for decoding the strings read from a different character set e.g. cp850<br>
<li><b>dev-([cdih]-)*defEncode</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defEncode" data-pattern="dev-.*defEncode">dev-([cdih]-)?defEncode</a><br>
defines a default for encoding the strings read (or after decoding from a different character set) e.g. utf8<br>
<li><b>dev-([cdih]-)*defPoll</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defPoll" data-pattern="dev-.*defPoll">dev-([cdih]-)?defPoll</a><br>
if set to 1 then all objects of this type will be included in the cyclic update by default. <br>
<li><b>dev-([cdih]-)*defShowGet</b></li>
if set to 1 then all objects of this type will have a visible get by default. <br>
<li><b>dev-([cdih]-)*defHint</b></li>
Example: attr MBTest dev-h-defPoll 1
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defPolldelay" data-pattern="dev-.*defPollDelay">dev-([cdih]-)?defPolldelay</a><br>
sets a default for obj-x-polldelay attributes
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defShowGet" data-pattern="dev-.*defShowGet">dev-([cdih]-)?defShowGet</a><br>
if set to 1 then all objects of this type will have a visible get by default.<br>
Example: attr MBTest dev-h-defShowGet 1
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defHint" data-pattern="dev-.*defHint">dev-([cdih]-)?defHint</a><br>
defines a default hint for all objects of this type
<li><b>dev-([cdih]-)*defSet</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?defSet" data-pattern="dev-.*defSet">dev-([cdih]-)?defSet</a><br>
defines a default for allowing set commands to all objects of this type
<li><b>dev-type-XYZ-unpack, -len, -encode, -decode, -revRegs, -bswapRegs, -format, -expr, -map</b></li>
Example: attr MBTest dev-h-defSet 1
</li>
<li><a id="ModbusAttr-attr-dev-type-XYZ-unpack" data-pattern="dev-type.*">dev-type-XYZ-unpack, -len, -encode, -decode, -revRegs, -bswapRegs, -format, -expr, -map</a><br>
define the unpack code, length and other details of a user defined data type. XYZ has to be replaced with the name of a user defined data type.
use obj-h123-type XYZ to assign this type to an object.<br>
<li><b>dev-([cdih]-)*allowShortResponses</b></li>
see <a href="#ModbusAttr-attr-obj-[ih][0-9]+-type">here</a>
</li>
<li><a id="ModbusAttr-attr-dev-([cdih]-)?allowShortResponses" data-pattern="dev-.*allowShortResponses">dev-([cdih]-)?allowShortResponses</a><br>
if set to 1 the module will accept a response with valid checksum but data lengh < lengh in header<br>
<li><b>dev-h-brokenFC3</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-d-brokenFC2">dev-d-brokenFC2</a><br>
if set to doepke the module will change the parsing of function code 2 responses for devices that
send an additional dummy zero byte before the correct response data byte<br>
</li>
<li><a id="ModbusAttr-attr-dev-h-brokenFC3">dev-h-brokenFC3</a><br>
if set to 1 the module will change the parsing of function code 3 and 4 responses for devices that
send the register address instead of the length in the response<br>
<li><b>dev-c-brokenFC5</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-c-brokenFC5">dev-c-brokenFC5</a><br>
if set the module will use the hex value specified here instead of ff00 as value 1 for setting coils<br>
<li><b>dev-timing-timeout</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-timing-timeout">dev-timing-timeout</a><br>
timeout for the device when a Fhem master waits for a slave response (defaults to 2 seconds)<br>
<li><b>dev-timing-serverTimeout</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-timing-serverTimeout">dev-timing-serverTimeout</a><br>
timeout for a TCP connected Fhem slave before it closes a TCP connection after inactivity<br>
<li><b>dev-timing-sendDelay</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-timing-sendDelay">dev-timing-sendDelay</a><br>
delay to enforce between sending two requests to the device. Default ist 0.1 seconds.<br>
<li><b>dev-timing-commDelay</b></li>
</li>
<li><a id="ModbusAttr-attr-dev-timing-commDelay">dev-timing-commDelay</a><br>
delay between the last read and a next request. Default ist 0.1 seconds.<br>
<li><b>queueMax</b></li>
</li>
<li><a id="ModbusAttr-attr-queueMax">queueMax</a><br>
max length of the queue for sending modbus requests as master, defaults to 200. <br>
This atribute should be used with devices connected through TCP or on physical
devices that are connected via serial lines but not on logical modbus devices that use another physical device as IODev.<br>
<li><b>nextOpenDelay</b></li>
</li>
<li><a id="ModbusAttr-attr-nextOpenDelay">nextOpenDelay</a><br>
delay for Modbus-TCP connections. This defines how long the module should wait after a failed TCP connection attempt before the next reconnection attempt. This defaults to 60 seconds.
<li><b>nextOpenDelay2</b></li>
</li>
<li><a id="ModbusAttr-attr-nextOpenDelay2">nextOpenDelay2</a><br>
delay for Modbus-TCP connections. This defines how long the module should wait after any TCP connection attempt before the next reconnection attempt. This defaults to 2 seconds.
<li><b>openTimeout</b></li>
</li>
<li><a id="ModbusAttr-attr-openTimeout">openTimeout</a><br>
timeout to be used when opening a Modbus TCP connection (defaults to 3)
<li><b>timeoutLogLevel</b></li>
</li>
<li><a id="ModbusAttr-attr-timeoutLogLevel">timeoutLogLevel</a><br>
log level that is used when logging a timeout. Defaults to 3.
<li><b>silentReconnect</b></li>
if set to 1, then it will set the loglevel for "disconnected" and "reappeared" messages to 4 instead of 3
<li><b>maxTimeoutsToReconnect</b></li>
</li>
<li><a id="ModbusAttr-attr-silentReconnect">silentReconnect</a><br>
if set to 1, then it will set the loglevel for "disconnected" and "reappeared" messages to 4 instead of 3.
This is especially useful when TCP slaves discoonect after an inactivity timeout.
</li>
<li><a id="ModbusAttr-attr-maxTimeoutsToReconnect">maxTimeoutsToReconnect</a><br>
this attribute is only valid for TCP connected devices. In such cases a disconnected device might stay undetected and lead to timeouts until the TCP connection is reopened. This attribute specifies after how many timeouts an automatic reconnect is tried.
<li><b>nonPrioritizedSet</b></li>
</li>
<li><a id="ModbusAttr-attr-closeAfterResponse">closeAfterResponse</a><br>
if set to 1, then Fhem as Master will close TCP connections to Slaves after it received the response
and automatically reopen the connection to the slave when the next request has to be sent.
</li>
<li><a id="ModbusAttr-attr-nonPrioritizedSet">nonPrioritizedSet</a><br>
if set to 1, then set commands will not be sent on the bus before other queued requests and the response will not be waited for.
<li><b>sortUpdate</b></li>
</li>
<li><a id="ModbusAttr-attr-sortUpdate">sortUpdate</a><br>
this attribute has become obsolte. The requests during a getUpdate cycle will always be sorted before beeing queued.
</li>
<li><a id="ModbusAttr-attr-cacheUpdateHash">cacheUpdateHash</a><br>
if this attribute is set to 1 then then Fhem as Modbus-Master will ignore any pollDelays, cache the list of combined objects to be requested
and request this list in all subsequent getUpdate rounds in the defined interval. It has no effect on explicit get commands.<br>
This will result in increased memory usage and potentially some performance increase.
</li>
<li><a id="ModbusAttr-attr-cacheParseInfo">cacheParseInfo</a><br>
if this attribute is set to 1 then then Fhem will cache the information regarding parsing each object in a hash
which results in increased memory usage and potentially some performance increase.
</li>
<li><b>propagateVerbose</b></li>
<li><a id="ModbusAttr-attr-propagateVerbose">propagateVerbose</a><br>
this attribute causes changes to the verbose attribute of a logical device to be propagated to the physical io device
or if the logical device is a relay device to the master device used by the relay.
<li><b>connectionsRoom</b></li>
</li>
<li><a id="ModbusAttr-attr-connectionsRoom">connectionsRoom</a><br>
defines to which room a TCP connection device for TCP slaves or relays is assigned to.
When a TCP slave accepts a connection then the new temporary connection device is by default assigned to the room "Connections".
If this attribute is set to "none" then no room attribute is set for connection devices by the module
and fhem will automatically use the room 'hidden'.
<li><b>serverIdExpr</b></li>
</li>
<li><a id="ModbusAttr-attr-serverIdExpr">serverIdExpr</a><br>
sets the server id response to be sent back as client if a server is requesting it via function code 17<br>
this is defiend as a perl expression for more flexibility.
<li><b>disable</b></li>
this is defiend as a perl expression for more flexibility.
</li>
<li><a id="ModbusAttr-attr-disable">disable</a><br>
stop communication with the device while this attribute is set to 1. For Modbus over TCP this also closes the TCP connection.
<br>
</ul>
<br>
</li>
</ul>
</ul>