Modbus

Aus FHEMWiki
Wechseln zu: Navigation, Suche
Modbus
Zweck / Funktion
Library or physical device to extract information from devices with a Modbus interface or send information to such devices
Allgemein
Typ Gerätemodul
Details
Dokumentation EN / DE
Support (Forum) Sonstiges
Modulname 98_Modbus.pm
Ersteller StefanStrobel (Forum / Wiki)


Modbus defines a physical modbus interface and library 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 Modbus module supports Modbus RTU over serial / RS485 lines as well as Modbus TCP and Modbus RTU over TCP. It defines read / write functions for Modbus holding registers, input registers, coils and discrete inputs.

See ModbusAttr if you don't want to use a library to develop your own module and if you are looking for a generic Modbus Module instead that can be configured with attributes.

Availability

The module has been checked in.

Prerequisites

This module requires the Device::SerialPort or Win32::SerialPort module if you want to communicate with modbus devices over a serial line.

Define of a modbus interface device for serial communication

define <name> Modbus <device>

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 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.

Example:

define ModBusLine Modbus /dev/ttyUSB1@9600

In this example the module opens the given serial interface and other logical modules like ModbusAttr or ModbusSET can access several Modbus devices connected to this bus concurrently.

Set-Commands

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 as a library. See ModbusSET for example.

Get-Commands

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 as a library.

Attributes

do_not_notify
readingFnAttributes
queueDelay
modify the delay used when sending requests to the device from the internal queue, defaults to 1 second
queueMax
max length of the send queue, defaults to 100

Writing modules for devices using this module as a library

Writing a module for a physical device with modbus interface is easy when you use the 98_Modbus.pm module as a library. To use this module as a library for other fhem modules you only have to define a data structure that defines the mapping between modbus data objects (holding registers, input registers, coils or discrete inputs) and fhem readings. Additionally the module needs to contain a few package and use statements and an initialize function at the beginning, that assigns a few special variables to point to functions of the Modbus base module.

Example for a module that is called ModbusSET:

package main;
use strict;
use warnings;

sub ModbusSET_Initialize($)
{
    my ($modHash) = @_;
    require "$attr{global}{modpath}/FHEM/98_Modbus.pm";
    $modHash->{parseInfo}  = \%SET10parseInfo;   # defines registers, inputs, coils etc. for this Modbus Defice
    $modHash->{deviceInfo} = \%SET10deviceInfo;  # defines properties of the device, defaults and supported function codes
    ModbusLD_Initialize($modHash);               # Generic function of the Modbus module does the rest
    $modHash->{AttrList} = $modHash->{AttrList}; # Standard Attributes like IODEv etc 
}

The name of the initialize-Function has to match the name of the module. In the above example this is ModbusSET_Initialize. Most of the steps needed in an initialize function are provided by the library function ModbusLD_Initialize. This function tells fhem to use the library functions for define, set, get and other typical functions in a module. See DevelopmentModuleIntro for more background information on writing fhem modules if you are curious.

Introduction to the parseInfo structure

Typically the data structure to map between data objects of the modbus device and fhem readings is named parseInfo with a part of the name of the module itself as prefix. In the example of the module 98_ModbusSET.pm which uses Modbus.pm to implement a module for SET Silent 10 heat pumps, the structure is called SET10parseInfo.

As an example a very simple definition of a parseInfo structure for a heat pump could look like this:

my %XYparseInfo = (
    "h256"  =>  {   reading => "Temp_Wasser_Ein",   # name of the reading for this value
                },
    "h258"  =>  {   reading => "Temp_Wasser_Aus",
                },
    "h770"  =>  {   reading => "Temp_Soll", 
                    min     => 10,                  # input validation for set: min value
                    max     => 32,                  # input validation for set: max value
                    set     => 1,                   # this value can be set
                }
);

This would be the main part of the module and map from holding register 256 to a fhem reading named Temp_Wasser_Ein, holding register 258 to Temp_Wasser_Aus and 770 to Temp_Soll.

All readings will be read from the device in an interval that the user can specify when he issues the define command for your module. The meaning of set => 1 is that the holding register 770 can also be written to with a set command. Fhem will check that the value written is not smaller than 10 and not bigger than 32 as specified above.

More complex example:

my %SET10parseInfo = (
    "h256"  =>  {   reading => "Temp_Wasser_Ein",   # name of the reading for this value
                    name    => "Pb1",               # internal name of this register in the hardware doc
                    expr    => '$val / 10',         # conversion of raw value to visible value 
                    len     => 1,
                },
    "h770"  =>  {   reading => "Temp_Soll", 
                    name    => "ST03",
                    expr    => '$val / 10',         # convert raw value to readable temp
                    setexpr => '$val * 10',         # expression to convert a set value to the internal value 
                    min     => 10,                  # input validation for set: min value
                    max     => 32,                  # input validation for set: max value
                    hint    => "8,10,20,25,28,29,30,30.5,31,31.5,32",
                    set     => 1,                   # this value can be set
                },
    "h771"  =>  {   reading => "Hysterese",         # Hex Adr 303
                    name    => "ST04",
                    expr    => '$val / 10',
                    setexpr => '$val * 10',
                    poll    => "x10",               # only poll every 10th iteration.
                    min     => 0.5,
                    max     => 3,
                    set     => 1,
                },
    "h777"  =>  {   reading => "Hyst_Mode",         # Hex Adr 0309
                    name    => "ST10",
                    map     => "0:mittig, 1:oberhalb, 2:unterhalb", 
                    poll    => "once",              # only poll once (or after a set)
                    set     => 1,
                },
    "i800"  =>  {   reading => "Voltage",           # Input register 
                    unpack  => "f>",                # this value is a float
                    len     => 2,                   # the float occupies two input registers, 800 and 801
                },
);

There are many more options that can be specified for each data object / reading. If these options are not specified, the base module assumes defaults that typically make sense. However if you want to modify the defaults, you can either define explicit values in the parseInfo structure or you can define another data structure typically called deviceInfo.

Introduction to the deviceInfo structure

The deviceInfo structure is completely optional. If you don't define it in your module, the base module takes default values that work in ost cases. If you only want to override a few of the defaults, you can just define them and leave other options or sections out. A simple device info structure that modifies some defaults could look like this:

my %SET10deviceInfo = (
    "timing"    => {
            timeout     =>  3,      # timeout is 3 seconds /default would be 2
            commDelay   =>  0.7,    # wait 0.7 seconds before sending after receiving
            sendDelay   =>  0.7,    # wait at least 0.7 seconds for another send
            }, 
    "c"     =>  {               
            read        =>  1,      # function code 1 to read coils (this could be omitted because it is the default anyways
            write       =>  5,      # dito
            },
    "h"     =>  {               
            read        =>  3,      
            write       =>  6,      
            defLen      =>  1,      # default legth is 1 object
            combine     =>  5,      
            defShowGet  =>  1,      
            defUnpack   =>  "s>",   # default data format is a signed 16 bit integer for holding registers 
            },
);

The deviceInfo structure contains five optional parts. Timing defines timing values and the remaining parts define settings or defaults for coils (c), discrete inputs (d), input registers (i) and holding registers (h).

for each modbus object type you can change what function code should be used to read or write to the object. This is completely optional and if nothing is specified, the base module assumes function codes 1,2,3 and 4 for reading as well as 5 and 6 for writing which works for many modbus devices. If you prefer to use function code 16 for writing to holding registers, you can specify "write => 16" in the "h" part.

usage of a module created this way

a logical module written this way will have a define command that can work in two ways. If your module would be called ModbusSET and it is using a serial line connection (Modbus RTU over RS485 oer over RS232):

define <iodevice> Modbus /dev/device@baudrate
define <name> ModbusSET <Id> <Interval> </code>

In this case, a physical serial interface device is defined first using the Modbus module. Then a device based on your module (ModbusSET in the example) is defined for each physical modbus device connected to the serial line. For a RS485 bus, several devices with different Ids can be connected to the same bus.

Example:

define ModbusRS485 Modbus /dev/rs485@9600
define PWP ModbusAttr 5 60

this defines the device and it will use the readings that you coded in the parseInfo data structure.

Alternatively your module would also support Modbus TCP or Modbus RTU over TCP with the following define syntax:

define <name> ModbusAttr <Id> <Interval> <Address:Port> <RTU|TCP>

In this case no serial interface device is necessary and your module connects to the modbus device directly via TCP using either Modbus TCP or Modbus RTU over TCP.

Example:

define PWP ModbusAttr 1 30 192.168.1.115:502 TCP

General information about data objects

Modbus devices can use many different ways to encode values in their data objects. A temperature might be stored multiplied with 10 as a 16 bit integer value in one holding register so you have to read the integer and divide it by 10 to get the real temperature value back. It might also be stored as a 32 bit float data type that spans two adjacent input registers. The modbus base module implements a very generic way to handle different encodings without real programming: It lets you define the Perl unpack code to convert a raw data string to a Perl value, a Perl expression to do further computation and a length in data objects.

This way a temperature stored in a 16 bit signed integer as the value multiplied by 10 can be described with the unpack code "s>" and the expression "$val / 10". A float value spanning 2 registers would be described with an unpack code "f>" and a len of 2. No expression is needed in this case. See Perldoc on the pack function for a detailed explation of pack and unpack codes.

The idea here is that you should be able to define any mapping, encoding, transformation or formatting of data objects without programming by simpy describing them.

All options in parseInfo

Most options here are optional and can be used if there is a need but they can also be omitted. If most readings require the same options and the option is different from the default, it is also possible to define a different default per modbus data object type in another data structure (see deviceInfo).

reading
name of the reading to be used in Fhem e.g. Temp_Wasser_ein
expr
perl expression to convert a string after it has been read. The original value is in $val e.g. $val / 10
map
a map string to convert an value from the device to a more readable output string or to convert a user input to the machine representation e.g. "0:mittig, 1:oberhalb, 2:unterhalb"
format
a format string for sprintf to format a value read, e.g. %.1f
len
number of Registers this value spans, can be 2 for a 32 bit float which is stored in 2 registers
unpack
defines the translation between data in the module and in the communication frame see the documentation of the perl pack function for details. example: "n" for an unsigned 16 bit value or "f>" for a float that is stored in two registers or "s>" for signed 16 bit integer in big endian format
showget
can be set to 1 to allow a Fhem get command to read this value from the device. All defined objects can be used in a get command that is issued on the command line. This parameter only controls if fhemweb will offer a get command for the object.
poll
defines if this value is included in the read that the module does every defined interval this can be changed by a user with an attribute
polldelay
if a value should not be read in each iteration (after interval has passed), this value can be set to an explicit time in seconds. The update function will then verify if this delay has elapsed since the last read of this object. If not, the read is skipped.
set
can be set to 1 to allow writing this value with a Fhem set-command
min
min value for input validation in a set command. If the user issues e.g. set Device Temp_Soll 10, Fhem will check if the given value 10 is bigger or equal the defined min and smaller or equal the defined max.
max
max value for input validation in a set command
hint
string for fhemweb to create a selection or slider
setexpr
per expression to convert an input string to the machine format before writing this is typically the reverse of the above expr, e.g. $val * 10
name
optional internal name of the value in the modbus documentation of the physical device, e.g. pb1


All options in deviceInfo

Keys in the timing section:

timeout
how long to wait for a response from the device, can be overwritten by attribute timeout in logical device. Defaults to two seconds if this is not specified
commDelay
minimal delay in secounds between two communications e.g. a read a the next write, can be overwritten with attribute commDelay if added to AttrList in _Initialize below defaults to 0.1 seconds if not specified
sendDelay
minimal delay in seconds between two sends, can be overwritten with the attribute sendDelay if added to AttrList in _Initialize function below. Defaults to 0.1 seconds if not specified

Keys per object type (c = coil, d = discrete input, i = input register, h = holding register)

read
function code to use for reading this object type (e.g. 3 for holding registers) defaults to function codes 1-4 depending on the object types if nothing else is specified (3 to read holding register, 1 to read coils and so on)
write
function code to use for writing this object type (e.g. 6 or 16 for holding registers) defaults to function codes 5 and 6 depending on the object types if nothing else is specified (6 to read holding register, 5 to write coils and so on)
defLen
default len for objects using this type (e.g. can be set to 2 if the device mainly provides float values that span 2 registers (2 times 16 Bit) can be overwritten in parseInfo per reading by specifying the key "len" defaults to 1 if not specified
defFormat
format string to do sprintf with the value can be overwritten in parseInfo per reading by specifying the key "format" if no format is specified here and none in parseInfo, the the reading is set without further formatting (which is typically fine)
defUnpack
default pack / unpack code to convert raw values, e.g. "n" for a 16 bit integer or "f>" for a big endian float can be overwritten in parseInfo per reading by specifying the key "unpack" if not specified here and not in parseInfo, then the raw value is interpreted as "n" which is 16 bit unsigned integer in big endian format
defPoll
defines that objects of this type should be polled by default unless specified otherwise in parseInfo or by attributes can be overwritten in parseInfo per reading by specifying the key defaultpoll if not specified here or in parseInfo, the object is not polled
defShowGet
defines that Fhemweb shows a Get option (by returning it as reslut to get ?) for objects of this type can be overwritten in parseInfo per reading by specifying the key showget defaults to 0.
combine
max number of registers that the device is willing to deliver in one read request. The modbus application layer protocol specification allows for more than 100 but most devices limit this to 5, 10 or some other number. This option defaults to 1 if not specified.

For an example of a full module that is based on the mechanisms described here see 98_ModbusSET.pm.

Attributes of your module

a module based on the base module / library 98_Modbus.pm can also allow the end user to modify properties of each reading if you want to allow it. All you have to do is to offer an attribute by adding its name to the variable $modHash->{AttrList} in your initialize function.

If for example you want to allow the user to modify the maximum value for the reading Temp_Soll, you can add "Temp_Soll-max " to this variable and the use can the set this attribute. The attribute takes precedence over the max potentially already defined in your parseInfo structure.

there are two ways that the base module accepts such readings. One is the reading name followed by "-" and the option to override, the alternative syntax is "obj-" followed by the first letter of an object type (c/d/h/i) and a decimal address just like the main key of an object in the parseInfo structure.

Instaed of allowing the attribute Temp_Soll-max for the max value of reading Temp_Soll which corresponds to holding register 770, you can alternatively add the attribute name "obj-h770-min " to $modHash->{AttrList}.

If the user is allowd to specify such attributes solely depends on the contents of the $modHash->{AttrList} variable. All the processing is already built into the base module.

If you want to allow the user the override the formatting of readings then you can add "obj-[cdih][1-9][0-9]*-format " as a regular expression that allows format specifications for all possible data objects.

The module 98_ModbusAttr for example is also based on 98_Modbus.pm and allows all possible attributes so the user can completely define his device with attributes and without a parseInfo or deviceInfo structure.

In the same way you can allow the user to override the device specific options and defaults with attributes that start with "dev-", followed by the section of the deviceInfo and the name of the option. If you want to allow the user to modify the function code to be used for writing holding registers, you can add the attribute "obj-h-write " and the user can then set this attribute to 6 or 16 as he prefers. It is up to the module author to decide if this makes sense.

An assignment that allows most options to the user could be:

    $modHash->{AttrList} = $modHash->{AttrList} . " " .
        "obj-[cdih][1-9][0-9]*-reading " .
        "obj-[cdih][1-9][0-9]*-name " .
        "obj-[cdih][1-9][0-9]*-set " .
        "obj-[cdih][1-9][0-9]*-min " .
        "obj-[cdih][1-9][0-9]*-max " .
        "obj-[cdih][1-9][0-9]*-hint " .
        "obj-[cdih][1-9][0-9]*-expr " .
        "obj-[cdih][1-9][0-9]*-map " .
        "obj-[cdih][1-9][0-9]*-setexpr " .
        "obj-[cdih][1-9][0-9]*-format " .
        "obj-[cdih][1-9][0-9]*-len " .
        "obj-[cdih][1-9][0-9]*-unpack " .
        "obj-[cdih][1-9][0-9]*-showget " .
        
        "obj-[cdih][1-9][0-9]*-poll " .
        "obj-[cdih][1-9][0-9]*-polldelay " .
        "poll-.* " .
        "polldelay-.* " .
        
        "dev-([cdih]-)*read " .
        "dev-([cdih]-)*write " .
        "dev-([cdih]-)*combine " .
        "dev-([cdih]-)*defLen " .
        "dev-([cdih]-)*defFormat " .
        "dev-([cdih]-)*defUnpack " .
        "dev-([cdih]-)*defPoll " .
        "dev-([cdih]-)*defShowGet " .
        "dev-timing-timeout " .
        "dev-timing-sendDelay " .
        "dev-timing-commDelay ";
}

Examples for logical device modules that use this base module

SDM220M
SDM630M
modules for energy meters from B+G E-Tech & EASTON written by Roger
UMG103
UMG604
modules for the UMG103 and UMG604 meters from Janitza
ModbusSET
module for the set silent heat pumps from Schmidt Energie Technik
ModbusAttr
generic modbus device module where the data objects, addresses, display formats, function codes and other things can be configured using Fhem attributes similar to HTTPMOD