openhab-addons/bundles/org.openhab.binding.modbus/DEVELOPERS.md
Fabian Wolter 568da33684
[modbus] BaseModbusThingHandler: Add ability to retrieve slave address (#9181)
* [modbus] BaseModbusThingHandler: Add ability to retrieve slave address
* Rework error handling

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
2020-12-04 21:24:55 -08:00

8.0 KiB

For Developers

Debugging an addon

Please follow IDE setup guide at https://www.openhab.org/docs/developer/ide/eclipse.html.

When configuring dependencies in openhab-distro/launch/app/pom.xml, add all dependencies, including the transitive dependencies:

<dependency>
    <groupId>org.openhab.addons.bundles</groupId>
    <artifactId>org.openhab.binding.modbus</artifactId>
    <version>${project.version}</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.openhab.addons.bundles</groupId>
    <artifactId>org.openhab.io.transport.modbus</artifactId>
    <version>${project.version}</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.openhab.osgiify</groupId>
    <artifactId>net.wimpi.jamod</artifactId>
    <version>1.2.4.OH</version>
    <scope>runtime</scope>
</dependency>

Testing Serial Implementation

You can use test serial slaves without any hardware on Linux using these steps:

  1. Set-up virtual null modem emulator using tty0tty
  2. Download diagslave and start modbus serial slave up using this command:
./diagslave -m rtu -a 1 -b 38400 -d 8 -s 1 -p none -4 10 /dev/pts/7
  1. Configure openHAB's modbus slave to connect to /dev/pts/8.

  2. Modify start.sh or start_debug.sh to include the unconventional port name by adding the following argument to java:

-Dgnu.io.rxtx.SerialPorts=/dev/pts/8

Naturally this is not the same thing as the real thing but helps to identify simple issues.

Testing TCP Implementation

  1. Download diagslave and start modbus tcp server (slave) using this command:
./diagslave -m tcp -a 1 -p 55502
  1. Configure openHAB's modbus slave to connect to 127.0.0.1:55502.

Writing Data

See this community post explaining how pollmb and diagslave can be used to debug modbus communication.

You can also use modpoll to write data:

# write value=5 to holding register 40001 (index=0 in the binding)
./modpoll -m tcp -a 1 -r 1 -t4 -p 502 127.0.0.1 5
# set coil 00001 (index=0 in the binding) to TRUE
./modpoll -m tcp -a 1 -r 1 -t0 -p 502 127.0.0.1 1
# write float32
./modpoll -m tcp -a 1 -r 1 -t4:float -p 502 127.0.0.1 3.14

Extending Modbus Binding

This Modbus binding can be extended by other OSGi bundles to add more specific support for Modbus enabled devices. To do so to you have to create a new OSGi bundle which has the same binding id as this binding. The best way is to use the ModbusBindingConstants.BINDING_ID constant.

Thing Handler

You will have to create one or more handler classes for the devices you want to support. For the modbus connection setup and handling you can use the Modbus TCP Slave or Modbus Serial Slave handlers. Your handler should use these handlers as bridges and you can set up your regular or one shot modbus requests to read from the slave. This is done by extending your ThingHandler by BaseModbusThingHandler. You can use the inherited methods submitOneTimePoll() and registerRegularPoll() to poll values and submitOneTimeWrite() to send values to a slave. The BaseModbusThingHandler takes care that every regular poll task is cancelled, when the Thing is disposed. Despite that, you can cancel the task manually by storing the return value of registerRegularPoll() and use it as an argument to unregisterRegularPoll().

Please keep in mind that these reads are asynchronous and they will call your callback once the read is done.

Once you have your data read from the modbus device you can parse and transform them then update your channels to publish these data to the openHAB system.

See the following example:

@NonNullByDefault
public class MyHandler extends BaseModbusThingHandler {
    public MyHandler(Thing thing) {
        super(thing);
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        if (command instanceof RefreshType) {
            ModbusReadRequestBlueprint blueprint = new ModbusReadRequestBlueprint(getSlaveId(),
                    ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 1, 2);

            submitOneTimePoll(blueprint, this::readSuccessful, this::readError);
        }
    }

    @Override
    public void modbusInitialize() {
        // do other Thing initialization

        ModbusReadRequestBlueprint blueprint = new ModbusReadRequestBlueprint(getSlaveId(),
                ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 1, 2);

        registerRegularPoll(blueprint, 1000, 0, this::readSuccessful, this::readError);
    }

    private void readSuccessful(AsyncModbusReadResult result) {
        result.getRegisters().ifPresent(registers -> {
            Optional<DecimalType> value = ModbusBitUtilities.extractStateFromRegisters(registers, 0, ValueType.INT16);
            // process value
        });
    }

    private void readError(AsyncModbusFailure<ModbusReadRequestBlueprint> error) {
        // set the Thing offline
    }
}

Discovery

If you write a device specific handler then adding discovery for this device is very welcome. You will have to write a discovery participant class which implements the ModbusDiscoveryParticipant interface and registers itself as a component. Example:


@Component
@NonNullByDefault
public class SunspecDiscoveryParticipant implements ModbusDiscoveryParticipant {
...
}

There are two methods you have to implement:

  • getSupportedThingTypeUIDs should return a list of the thing type UIDs that are supported by this discovery participant. This is fairly straightforward.

  • startDiscovery method will be called when a discovery process has began. This method receives two parameters:

    • ModbusEndpointThingHandler is the endpoint's handler that should be tested if it is known by your bundle. You can start your read requests against this handler.

    • ModbusDiscoveryListener this listener instance should be used to report any known devices found and to notify the main discovery process when your binding has finished the discovery.

Please try to avoid write requests to the endpoint because it could be some unknown device that write requests could misconfigure.

When a known device is found a DiscoveryResult object has to be created then the thingDiscovered method has to be called. The DiscoveryResult supports properties, and you should use this to store any data that will be useful when the actual thing will be created. For example you could store the start Modbus address of the device or vendor/model informations.

When the discovery process is finished either by detecting a device or by realizing it is not supported you should call the discoveryFinished method. This will tear down any resources allocated for the discovery process.

Discovery Architecture

The following diagram shows the concept how discovery is implemented in this binding. (Note that some intermediate classes and interfaces are not shown for clarity.)

Discovery architecture

As stated above the discovery process can be extended by OSGi bundles. For this they have to define their own ModbusDisvoceryParticipant that gets registered at the ModbusDiscoveryService. This object also keeps track of any of the Modbus handlers. Handler level discovery logic is implemented in the ModbusEndpointDiscoveryService which gets instantiated for each Modbus BridgeHandler.

The communication flow is detailed in the diagram below:

Discovery process

As can be seen the process is initiated by the ModbusDiscoveryService which calls each of the ModbusEndpointDiscoveryService instances to start the discovery on the available participants. Then a reference to the ThingHandler is passed to each of the participants who can use this to do the actual discovery.

Any things discovered are reported back in this chain and ultimately sent to openHAB core.