[modbus] Add convenience class for ThingHandlers (#8634)

* [modbus] Add convenience class for ThingHandlers

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
Fabian Wolter 2020-11-24 03:38:07 +01:00 committed by GitHub
parent 4911fd0c4a
commit d0b5267ff3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 263 additions and 2 deletions

View File

@ -75,21 +75,70 @@ You can also use `modpoll` to write data:
./modpoll -m tcp -a 1 -r 1 -t4:float -p 502 127.0.0.1 3.14
```
## Extending Modbus binding
## 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 by creating a `BasicPollTaskImpl` and submitting it using the `ModbusManager` `submitOneTimePoll` and `registerRegularPoll` methods.
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:
```java
@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(42,
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 1, 2);
submitOneTimePoll(blueprint, this::readSuccessful, this::readError);
}
}
@Override
public void initialize() {
super.initialize();
// do other Thing initialization
ModbusReadRequestBlueprint blueprint = new ModbusReadRequestBlueprint(42,
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.

View File

@ -0,0 +1,212 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.handler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.io.transport.modbus.ModbusCommunicationInterface;
import org.openhab.io.transport.modbus.ModbusFailureCallback;
import org.openhab.io.transport.modbus.ModbusReadCallback;
import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.io.transport.modbus.ModbusWriteCallback;
import org.openhab.io.transport.modbus.ModbusWriteRequestBlueprint;
import org.openhab.io.transport.modbus.PollTask;
import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
/**
* This is a convenience class to interact with the Thing's {@link ModbusCommunicationInterface}.
*
* @author Fabian Wolter - Initial contribution
*
*/
@NonNullByDefault
public abstract class BaseModbusThingHandler extends BaseThingHandler {
private List<PollTask> periodicPollers = Collections.synchronizedList(new ArrayList<>());
private List<Future<?>> oneTimePollers = Collections.synchronizedList(new ArrayList<>());
private volatile boolean initialized;
public BaseModbusThingHandler(Thing thing) {
super(thing);
}
/**
* This method must be invoked in the base class' initialize() method before any other initialization is done.
* It will throw an unchecked exception if the {@link ModbusCommunicationInterface} is not accessible (fail-fast).
* This prevents any further initialization of the Thing. The framework will set the ThingStatus to
* HANDLER_INITIALIZING_ERROR and display the exception's message.
*/
@Override
public void initialize() {
getModbus();
initialized = true;
}
/**
* Register regularly polled task. The method returns immediately, and the execution of the poll task will happen in
* the background.
*
* One can register only one regular poll task for triplet of (endpoint, request, callback).
*
* @param request request to send
* @param pollPeriodMillis poll interval, in milliseconds
* @param initialDelayMillis initial delay before starting polling, in milliseconds
* @param callback callback to call with data
* @param callback callback to call in case of failure
* @return poll task representing the regular poll
* @throws IllegalStateException when this communication has been closed already
*/
public PollTask registerRegularPoll(ModbusReadRequestBlueprint request, long pollPeriodMillis,
long initialDelayMillis, ModbusReadCallback resultCallback,
ModbusFailureCallback<ModbusReadRequestBlueprint> failureCallback) {
checkInitialized();
PollTask task = getModbus().registerRegularPoll(request, pollPeriodMillis, initialDelayMillis, resultCallback,
failureCallback);
periodicPollers.add(task);
return task;
}
/**
* Unregister regularly polled task
*
* If this communication interface is closed already, the method returns immediately with false return value
*
* @param task poll task to unregister
* @return whether poll task was unregistered. Poll task is not unregistered in case of unexpected errors or
* in the case where the poll task is not registered in the first place
*/
public boolean unregisterRegularPoll(PollTask task) {
periodicPollers.remove(task);
return getModbus().unregisterRegularPoll(task);
}
/**
* Submit one-time poll task. The method returns immediately, and the execution of the poll task will happen in
* background.
*
* @param request request to send
* @param callback callback to call with data
* @param callback callback to call in case of failure
* @return future representing the polled task
* @throws IllegalStateException when this communication has been closed already
*/
public Future<?> submitOneTimePoll(ModbusReadRequestBlueprint request, ModbusReadCallback resultCallback,
ModbusFailureCallback<ModbusReadRequestBlueprint> failureCallback) {
checkInitialized();
Future<?> future = getModbus().submitOneTimePoll(request, resultCallback, failureCallback);
oneTimePollers.add(future);
oneTimePollers.removeIf(Future::isDone);
return future;
}
/**
* Submit one-time write task. The method returns immediately, and the execution of the task will happen in
* background.
*
* @param request request to send
* @param callback callback to call with response
* @param callback callback to call in case of failure
* @return future representing the task
* @throws IllegalStateException when this communication has been closed already
*/
public Future<?> submitOneTimeWrite(ModbusWriteRequestBlueprint request, ModbusWriteCallback resultCallback,
ModbusFailureCallback<ModbusWriteRequestBlueprint> failureCallback) {
checkInitialized();
Future<?> future = getModbus().submitOneTimeWrite(request, resultCallback, failureCallback);
oneTimePollers.add(future);
oneTimePollers.removeIf(Future::isDone);
return future;
}
/**
* Get endpoint associated with this communication interface
*
* @return modbus slave endpoint
*/
public ModbusSlaveEndpoint getEndpoint() {
return getModbus().getEndpoint();
}
/**
* Retrieves the {@link ModbusCommunicationInterface} and does some validity checking.
* Sets the ThingStatus to offline if it couldn't be retrieved and throws an unchecked exception.
*
* The unchecked exception should not be caught by the implementing class, as the initialization of the Thing
* already fails if the {@link ModbusCommunicationInterface} cannot be retrieved.
*
* @throws IllegalStateException if the {@link ModbusCommunicationInterface} couldn't be retrieved.
* @return the {@link ModbusCommunicationInterface}
*/
private ModbusCommunicationInterface getModbus() {
try {
Bridge bridge = getBridge();
if (bridge == null) {
throw new IllegalStateException("Thing has no Bridge set");
}
BridgeHandler handler = bridge.getHandler();
if (handler instanceof ModbusEndpointThingHandler) {
ModbusCommunicationInterface communicationInterface = ((ModbusEndpointThingHandler) handler)
.getCommunicationInterface();
if (communicationInterface == null) {
throw new IllegalStateException("Failed to retrieve Modbus communication interface");
} else {
return communicationInterface;
}
} else {
throw new IllegalStateException("Bridge is not a Modbus bridge: " + handler);
}
} catch (IllegalStateException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Modbus initialization failed: " + e.getMessage());
throw e;
}
}
private void checkInitialized() {
if (!initialized) {
throw new IllegalStateException(
getClass().getSimpleName() + " not initialized. Please call super.initialize().");
}
}
@Override
public void dispose() {
oneTimePollers.forEach(p -> p.cancel(true));
oneTimePollers.clear();
ModbusCommunicationInterface modbus = getModbus();
periodicPollers.forEach(p -> modbus.unregisterRegularPoll(p));
periodicPollers.clear();
super.dispose();
}
}