From a4bc9ad716f5169535b6bc12aad2c7866eef4860 Mon Sep 17 00:00:00 2001 From: Sami Salonen Date: Fri, 10 Jul 2020 03:31:31 +0300 Subject: [PATCH] [modbus] Modbus transport API simplification (#7994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [modbus] connection closing behaviour finetuned The binding closes TCP/serial connections connections as per user configuration. It is either - every time, immediately after a transaction (reconnectAfterMillis=0) - *after* a read/write transaction, if the connection has been open "too long" (configurable with reconnectAfterMillis) We have an obvious downside to this simple logic -- the connection can remain open "indefinitely" if there are no transactions occurring. With Modbus we are quite often dealing with PLCs and other embedded systems with limited resources, and "reserving" the connection is not always something we want. Previously (2.5.x branch) connections were also closed when - a regular poll task was unregistered (i.e. we stop reading regularly from a modbus server ("slave"). Since most users have regular polling in place, so this ensured that connections do get closed. In this commit, the behaviour is adjusted such that connections are closed when last communication interface pointing to the server/serial port (i.e. "endpoint") is close()'d. With modbus binding this basically means when the tcp or serial thing is removed / disabled, the connections is closed/freed, but only if it is the last thing pointing to that server or serial port. Since modbus.sunspec binding reuses modbus serial & tcp endpoint things, the same note applies for modbus.sunspec binding. This is change in functional behaviour but in a way is logical. We can further introduce to have "delayed"/"deferred"/"debounce" connection closing connections as per the setting reconnectAfterMillis even in situations where communication interface is still open. * [modbus] Check and disconnect idle connections without transactions * [modbus] mvn spotless:apply * [modbubs] Fixed log message * [modbus] Race condition fix The CountDownLatch was used as a guard (latch.await) in many tests to wait for callbacks to be called before proceeding with assertions. Since the latch was countDown() beginning of the callback, we introduced a race condition with the subsequent assertions and updating the other counters used in the subsequent assertions. This commit updates the CountDownLatch as the last step of the callback, resolving the race condition. * [modbus] small test fix * [modbus] readcallback changed to nonnull * [modbus] Refactored ModbusCommunicationInterface to have seperate callback for result and failure However I had to dig deep to reach all the affected parts. Also there is a new callback, and a new "result" type to communicate the failures. * [modbus] SmokeTest refactored to new api * [modbus] Modbus bundle refactored to use the new api * [modbus][sunspec] refactored sunspec bundle to use the new modbus API * [modbus] refactor modbus tests to use the new api * [modbus] Removed ModbusWriteCallback interface from ModbusDataThingHandler Also reset ModbusDataThingHandler testWriteHandling generic to it's original form * [modbus] ModbusDataThingHandler does not implement ModbusReadCallback anymore Instead it has a public onReadResult method. ModbusPollerThingHandler changed to work with ModbusDataThingHandler children. * [modbus] Fixed caching in ModbusPollerThingHandler * [modbus] read modbus data as Optionals * [modbus] toString for PollResult * [modbus] fixed confusing variable name * [modbus] Disallow null callbacks * [modbus] mvn spotless:apply * [modbus] Removing Nullable decorations * [modbus] submitOneTimeWrite simplification * [modbus] Less verbose logging * [modbus] submitOneTimePoll simplification * [modbus] Less verbose logging * [modbus] Many null warnings removed * [modbus] Fix: no need for a @NonNull annotation because it is default * [modbus] Removing unneeded Nullable, using final in immutables * [modbus] Explicit functional interface * [modbus] @Nullable and @NonNullByDefault aligned with coding conventions * [modbus] Collections.emptyMap instead of allocating new map every time. Signed-off-by: Sami Salonen Co-authored-by: Nagy Attila Gábor --- .../internal/SunSpecHandlerFactory.java | 24 +- .../discovery/SunspecDiscoveryProcess.java | 110 +- .../handler/AbstractSunSpecHandler.java | 94 +- .../internal/handler/InverterHandler.java | 5 +- .../internal/handler/MeterHandler.java | 5 +- .../internal/ModbusDiscoveryService.java | 3 +- .../handler/ModbusEndpointThingHandler.java | 20 +- .../ModbusPollerThingHandler.java} | 289 +++-- ...dKeyValue.java => AtomicStampedValue.java} | 41 +- .../modbus/internal/ModbusHandlerFactory.java | 11 +- .../modbus/internal/Transformation.java | 9 +- .../config/ModbusDataConfiguration.java | 21 +- .../config/ModbusPollerConfiguration.java | 3 +- .../config/ModbusSerialConfiguration.java | 18 +- .../config/ModbusTcpConfiguration.java | 3 +- .../AbstractModbusEndpointThingHandler.java | 76 +- .../handler/ModbusDataThingHandler.java | 208 ++-- .../handler/ModbusPollerThingHandler.java | 51 - .../handler/ModbusSerialThingHandler.java | 13 +- .../handler/ModbusTcpThingHandler.java | 18 +- .../internal/AtomicStampedKeyValueTest.java | 104 +- .../transport/modbus/AsyncModbusFailure.java | 65 ++ .../modbus/AsyncModbusReadResult.java | 93 ++ .../modbus/AsyncModbusWriteResult.java | 66 ++ .../io/transport/modbus/BasicBitArray.java | 85 -- .../BasicModbusReadRequestBlueprint.java | 107 -- .../transport/modbus/BasicModbusRegister.java | 69 -- .../modbus/BasicModbusRegisterArray.java | 78 -- .../BasicModbusWriteCoilRequestBlueprint.java | 135 --- ...icModbusWriteRegisterRequestBlueprint.java | 104 -- .../openhab/io/transport/modbus/BitArray.java | 101 +- .../transport/modbus/ModbusBitUtilities.java | 38 +- .../modbus/ModbusCommunicationInterface.java | 96 ++ .../modbus/ModbusFailureCallback.java | 31 + .../io/transport/modbus/ModbusManager.java | 73 +- .../modbus/ModbusManagerListener.java | 36 - .../transport/modbus/ModbusReadCallback.java | 27 +- .../modbus/ModbusReadRequestBlueprint.java | 114 +- .../io/transport/modbus/ModbusRegister.java | 50 +- .../transport/modbus/ModbusRegisterArray.java | 63 +- .../modbus/ModbusRequestBlueprint.java | 63 - ...allback.java => ModbusResultCallback.java} | 2 +- .../transport/modbus/ModbusWriteCallback.java | 23 +- .../ModbusWriteCoilRequestBlueprint.java | 96 +- .../ModbusWriteRegisterRequestBlueprint.java | 84 +- .../modbus/ModbusWriteRequestBlueprint.java | 17 +- .../openhab/io/transport/modbus/PollTask.java | 3 +- .../io/transport/modbus/TaskWithEndpoint.java | 15 +- .../io/transport/modbus/WriteTask.java | 3 +- .../ModbusConnectionException.java | 2 +- .../ModbusSlaveErrorResponseException.java | 2 +- .../ModbusSlaveIOException.java | 2 +- .../ModbusTransportException.java | 2 +- ...expectedResponseFunctionCodeException.java | 2 +- ...ModbusUnexpectedResponseSizeException.java | 2 +- ...odbusUnexpectedTransactionIdException.java | 2 +- .../BasicPollTask.java} | 44 +- .../modbus/{ => internal}/BasicWriteTask.java | 26 +- .../internal/BitArrayWrappingBitVector.java | 69 -- .../modbus/internal/ModbusConnectionPool.java | 3 +- .../modbus/internal/ModbusLibraryWrapper.java | 37 +- .../modbus/internal/ModbusManagerImpl.java | 390 ++++--- .../modbus/internal/ModbusPoolConfig.java | 5 + ...ModbusSlaveErrorResponseExceptionImpl.java | 2 +- .../internal/ModbusSlaveIOExceptionImpl.java | 2 +- .../RegisterArrayWrappingInputRegister.java | 88 -- .../ModbusSlaveConnectionEvictionPolicy.java | 35 + .../ModbusSlaveConnectionFactoryImpl.java | 74 +- .../json/WriteRequestJsonUtilities.java | 19 +- .../modbus/test/BasicBitArrayTest.java | 14 +- ...tilitiesExtractStateFromRegistersTest.java | 6 +- ...ilitiesExtractStringFromRegistersTest.java | 6 +- .../io/transport/modbus/test/SmokeTest.java | 1014 +++++++++-------- .../modbus/tests/AbstractModbusOSGiTest.java | 41 +- .../modbus/tests/ModbusDataHandlerTest.java | 154 ++- .../tests/ModbusPollerThingHandlerTest.java | 287 +++-- .../tests/ModbusTcpThingHandlerTest.java | 147 ++- 77 files changed, 2582 insertions(+), 2758 deletions(-) rename bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/{internal/handler/ModbusPollerThingHandlerImpl.java => handler/ModbusPollerThingHandler.java} (57%) rename bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/{AtomicStampedKeyValue.java => AtomicStampedValue.java} (73%) delete mode 100644 bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusPollerThingHandler.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusFailure.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusReadResult.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusWriteResult.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicBitArray.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusReadRequestBlueprint.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusRegister.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusRegisterArray.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusWriteCoilRequestBlueprint.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusWriteRegisterRequestBlueprint.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusCommunicationInterface.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusFailureCallback.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusManagerListener.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRequestBlueprint.java rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{ModbusCallback.java => ModbusResultCallback.java} (93%) rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{ => exception}/ModbusConnectionException.java (96%) rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{ => exception}/ModbusSlaveErrorResponseException.java (98%) rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{ => exception}/ModbusSlaveIOException.java (93%) rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{ => exception}/ModbusTransportException.java (93%) rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{ => exception}/ModbusUnexpectedResponseFunctionCodeException.java (96%) rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{ => exception}/ModbusUnexpectedResponseSizeException.java (96%) rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{ => exception}/ModbusUnexpectedTransactionIdException.java (96%) rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{BasicPollTaskImpl.java => internal/BasicPollTask.java} (61%) rename bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/{ => internal}/BasicWriteTask.java (62%) delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BitArrayWrappingBitVector.java delete mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/RegisterArrayWrappingInputRegister.java create mode 100644 bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/pooling/ModbusSlaveConnectionEvictionPolicy.java diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecHandlerFactory.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecHandlerFactory.java index db5ea79984427..14d69d4bd3a10 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecHandlerFactory.java +++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecHandlerFactory.java @@ -23,10 +23,7 @@ import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; import org.openhab.binding.modbus.sunspec.internal.handler.InverterHandler; import org.openhab.binding.modbus.sunspec.internal.handler.MeterHandler; -import org.openhab.io.transport.modbus.ModbusManager; -import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,23 +42,6 @@ public class SunSpecHandlerFactory extends BaseThingHandlerFactory { */ private final Logger logger = LoggerFactory.getLogger(SunSpecHandlerFactory.class); - /** - * Reference to the modbus manager - */ - private ModbusManager manager; - - /** - * This factory needs a reference to the ModbusManager wich is provided - * by the org.openhab.io.transport.modbus bundle. Please make - * sure it's installed and enabled before using this bundle - * - * @param manager reference to the ModbusManager. We use this for modbus communication - */ - @Activate - public SunSpecHandlerFactory(@Reference ModbusManager manager) { - this.manager = manager; - } - @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.containsValue(thingTypeUID); @@ -75,12 +55,12 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { || thingTypeUID.equals(THING_TYPE_INVERTER_SPLIT_PHASE) || thingTypeUID.equals(THING_TYPE_INVERTER_THREE_PHASE)) { logger.debug("New InverterHandler created"); - return new InverterHandler(thing, manager); + return new InverterHandler(thing); } else if (thingTypeUID.equals(THING_TYPE_METER_SINGLE_PHASE) || thingTypeUID.equals(THING_TYPE_METER_SPLIT_PHASE) || thingTypeUID.equals(THING_TYPE_METER_WYE_PHASE) || thingTypeUID.equals(THING_TYPE_METER_DELTA_PHASE)) { logger.debug("New MeterHandler created"); - return new MeterHandler(thing, manager); + return new MeterHandler(thing); } return null; diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/discovery/SunspecDiscoveryProcess.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/discovery/SunspecDiscoveryProcess.java index a84febdf4d3fa..74087f06bb1cc 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/discovery/SunspecDiscoveryProcess.java +++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/discovery/SunspecDiscoveryProcess.java @@ -32,18 +32,14 @@ import org.openhab.binding.modbus.sunspec.internal.dto.CommonModelBlock; import org.openhab.binding.modbus.sunspec.internal.dto.ModelBlock; import org.openhab.binding.modbus.sunspec.internal.parser.CommonModelParser; -import org.openhab.io.transport.modbus.BasicModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.BasicPollTaskImpl; -import org.openhab.io.transport.modbus.BitArray; +import org.openhab.io.transport.modbus.AsyncModbusFailure; import org.openhab.io.transport.modbus.ModbusBitUtilities; +import org.openhab.io.transport.modbus.ModbusCommunicationInterface; import org.openhab.io.transport.modbus.ModbusConstants.ValueType; -import org.openhab.io.transport.modbus.ModbusReadCallback; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; import org.openhab.io.transport.modbus.ModbusRegisterArray; -import org.openhab.io.transport.modbus.ModbusSlaveErrorResponseException; -import org.openhab.io.transport.modbus.PollTask; -import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; +import org.openhab.io.transport.modbus.exception.ModbusSlaveErrorResponseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,11 +64,6 @@ public class SunspecDiscoveryProcess { */ private final ModbusEndpointThingHandler handler; - /** - * The endpoint where we can reach the device - */ - private final ModbusSlaveEndpoint endpoint; - /** * Listener for the discovered devices. We get this * from the main discovery service, and it is used to @@ -116,6 +107,11 @@ public class SunspecDiscoveryProcess { */ private @Nullable CommonModelBlock lastCommonBlock = null; + /** + * Communication interface to the endpoint + */ + private ModbusCommunicationInterface comms; + /** * New instances of this class should get a reference to the handler * @@ -125,9 +121,9 @@ public SunspecDiscoveryProcess(ModbusEndpointThingHandler handler, ModbusDiscove throws EndpointNotInitializedException { this.handler = handler; - ModbusSlaveEndpoint endpoint = this.handler.asSlaveEndpoint(); - if (endpoint != null) { - this.endpoint = endpoint; + ModbusCommunicationInterface localComms = handler.getCommunicationInterface(); + if (localComms != null) { + this.comms = localComms; } else { throw new EndpointNotInitializedException(); } @@ -158,30 +154,13 @@ public void detectModel() { baseAddress = possibleAddresses.poll(); logger.trace("Beginning scan for SunSpec device at address {}", baseAddress); - BasicModbusReadRequestBlueprint request = new BasicModbusReadRequestBlueprint(slaveId, + ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, baseAddress, // Start address SUNSPEC_ID_SIZE, // number or words to return maxTries); - PollTask task = new BasicPollTaskImpl(endpoint, request, new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - headerReceived(registers); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { - handleError(error); - } - - @Override - public void onBits(@Nullable ModbusReadRequestBlueprint request, @Nullable BitArray bits) { - // don't care, we don't expect this result - } - }); - - handler.getManagerRef().get().submitOneTimePoll(task); + comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::headerReceived), + this::handleError); } /** @@ -210,30 +189,13 @@ private void headerReceived(ModbusRegisterArray registers) { */ private void lookForModelBlock() { - BasicModbusReadRequestBlueprint request = new BasicModbusReadRequestBlueprint(slaveId, + ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, baseAddress, // Start address MODEL_HEADER_SIZE, // number or words to return maxTries); - PollTask task = new BasicPollTaskImpl(endpoint, request, new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - modelBlockReceived(registers); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { - handleError(error); - } - - @Override - public void onBits(@Nullable ModbusReadRequestBlueprint request, @Nullable BitArray bits) { - // don't care, we don't expect this result - } - }); - - handler.getManagerRef().get().submitOneTimePoll(task); + comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::modelBlockReceived), + this::handleError); } /** @@ -280,30 +242,13 @@ private void modelBlockReceived(ModbusRegisterArray registers) { * @param block */ private void readCommonBlock(ModelBlock block) { - BasicModbusReadRequestBlueprint request = new BasicModbusReadRequestBlueprint(slaveId, + ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, block.address, // Start address block.length, // number or words to return maxTries); - PollTask task = new BasicPollTaskImpl(endpoint, request, new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - parseCommonBlock(registers); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { - handleError(error); - } - - @Override - public void onBits(@Nullable ModbusReadRequestBlueprint request, @Nullable BitArray bits) { - // don't care, we don't expect this result - } - }); - - handler.getManagerRef().get().submitOneTimePoll(task); + comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::parseCommonBlock), + this::handleError); } /** @@ -367,25 +312,22 @@ private void parsingFinished() { /** * Handle errors received during communication */ - private void handleError(Exception error) { - String msg = ""; - String cls = ""; - - if (blocksFound > 1 && error instanceof ModbusSlaveErrorResponseException) { - int code = ((ModbusSlaveErrorResponseException) error).getExceptionCode(); + private void handleError(AsyncModbusFailure failure) { + if (blocksFound > 1 && failure.getCause() instanceof ModbusSlaveErrorResponseException) { + int code = ((ModbusSlaveErrorResponseException) failure.getCause()).getExceptionCode(); if (code == ModbusSlaveErrorResponseException.ILLEGAL_DATA_ACCESS || code == ModbusSlaveErrorResponseException.ILLEGAL_DATA_VALUE) { // It is very likely that the slave does not report an end block (0xffff) after the main blocks // so we treat this situation as normal. logger.debug( - "Seems like slave device does not report an end block. Continouing with the dectected blocks"); + "Seems like slave device does not report an end block. Continuing with the dectected blocks"); parsingFinished(); return; } } - cls = error.getClass().getName(); - msg = error.getMessage(); + String cls = failure.getCause().getClass().getName(); + String msg = failure.getCause().getMessage(); logger.warn("Error with read at address {}: {} {}", baseAddress, cls, msg); diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/AbstractSunSpecHandler.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/AbstractSunSpecHandler.java index 1a9fd8dc879a1..2be6d1ad14d59 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/AbstractSunSpecHandler.java +++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/AbstractSunSpecHandler.java @@ -38,16 +38,12 @@ import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler; import org.openhab.binding.modbus.sunspec.internal.SunSpecConfiguration; import org.openhab.binding.modbus.sunspec.internal.dto.ModelBlock; -import org.openhab.io.transport.modbus.BasicModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.BasicPollTaskImpl; -import org.openhab.io.transport.modbus.BitArray; -import org.openhab.io.transport.modbus.ModbusManager; -import org.openhab.io.transport.modbus.ModbusReadCallback; +import org.openhab.io.transport.modbus.AsyncModbusFailure; +import org.openhab.io.transport.modbus.ModbusCommunicationInterface; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.PollTask; -import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,29 +81,23 @@ public abstract class AbstractSunSpecHandler extends BaseThingHandler { private volatile @Nullable PollTask pollTask = null; /** - * This is the slave endpoint we're connecting to + * Communication interface to the slave endpoint we're connecting to */ - protected volatile @Nullable ModbusSlaveEndpoint endpoint = null; + protected volatile @Nullable ModbusCommunicationInterface comms = null; /** * This is the slave id, we store this once initialization is complete */ private volatile int slaveId; - /** - * Reference to the modbus manager - */ - protected final ModbusManager managerRef; - /** * Instances of this handler should get a reference to the modbus manager * * @param thing the thing to handle * @param managerRef the modbus manager */ - public AbstractSunSpecHandler(Thing thing, ModbusManager managerRef) { + public AbstractSunSpecHandler(Thing thing) { super(thing); - this.managerRef = managerRef; } /** @@ -142,7 +132,7 @@ private void startUp() { connectEndpoint(); - if (endpoint == null || config == null) { + if (comms == null || config == null) { logger.debug("Invalid endpoint/config/manager ref for sunspec handler"); return; } @@ -281,7 +271,7 @@ public int getSlaveId() { * Get a reference to the modbus endpoint */ private void connectEndpoint() { - if (endpoint != null) { + if (comms != null) { return; } @@ -297,13 +287,12 @@ private void connectEndpoint() { try { slaveId = slaveEndpointThingHandler.getSlaveId(); - - endpoint = slaveEndpointThingHandler.asSlaveEndpoint(); + comms = slaveEndpointThingHandler.getCommunicationInterface(); } catch (EndpointNotInitializedException e) { // this will be handled below as endpoint remains null } - if (endpoint == null) { + if (comms == null) { @SuppressWarnings("null") String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse(""); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, @@ -317,7 +306,8 @@ private void connectEndpoint() { * Remove the endpoint if exists */ private void unregisterEndpoint() { - endpoint = null; + // Comms will be close()'d by endpoint thing handler + comms = null; } /** @@ -330,52 +320,25 @@ private synchronized void registerPollTask(ModelBlock mainBlock) { throw new IllegalStateException("pollTask should be unregistered before registering a new one!"); } @Nullable - ModbusSlaveEndpoint myendpoint = endpoint; + ModbusCommunicationInterface mycomms = comms; @Nullable SunSpecConfiguration myconfig = config; - if (myconfig == null || myendpoint == null) { + if (myconfig == null || mycomms == null) { throw new IllegalStateException("registerPollTask called without proper configuration"); } logger.debug("Setting up regular polling"); - BasicModbusReadRequestBlueprint request = new BasicModbusReadRequestBlueprint(getSlaveId(), + ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(), ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries); - pollTask = new BasicPollTaskImpl(myendpoint, request, new ModbusReadCallback() { - - @Override - public void onRegisters(@Nullable ModbusReadRequestBlueprint request, - @Nullable ModbusRegisterArray registers) { - if (registers == null) { - logger.debug("Received empty register array on poll"); - return; - } - - handlePolledData(registers); - - if (getThing().getStatus() != ThingStatus.ONLINE) { - updateStatus(ThingStatus.ONLINE); - } - } - - @Override - public void onError(@Nullable ModbusReadRequestBlueprint request, @Nullable Exception error) { - handleError(error); - } - - @Override - public void onBits(@Nullable ModbusReadRequestBlueprint request, @Nullable BitArray bits) { - // don't care, we don't expect this result - } - }); - long refreshMillis = myconfig.getRefreshMillis(); - @Nullable - PollTask task = pollTask; - if (task != null) { - managerRef.registerRegularPoll(task, refreshMillis, 1000); - } + pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> { + result.getRegisters().ifPresent(this::handlePolledData); + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + }, this::handleError); } /** @@ -408,25 +371,24 @@ private synchronized void unregisterPollTask() { return; } logger.debug("Unregistering polling from ModbusManager"); - managerRef.unregisterRegularPoll(task); - + @Nullable + ModbusCommunicationInterface mycomms = comms; + if (mycomms != null) { + mycomms.unregisterRegularPoll(task); + } pollTask = null; } /** * Handle errors received during communication */ - protected void handleError(@Nullable Exception error) { + protected void handleError(AsyncModbusFailure failure) { // Ignore all incoming data and errors if configuration is not correct if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) { return; } - String msg = ""; - String cls = ""; - if (error != null) { - cls = error.getClass().getName(); - msg = error.getMessage(); - } + String msg = failure.getCause().getMessage(); + String cls = failure.getCause().getClass().getName(); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, String.format("Error with read: %s: %s", cls, msg)); } diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/InverterHandler.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/InverterHandler.java index 25c582553fde7..10e9ac99e64cd 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/InverterHandler.java +++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/InverterHandler.java @@ -25,7 +25,6 @@ import org.openhab.binding.modbus.sunspec.internal.InverterStatus; import org.openhab.binding.modbus.sunspec.internal.dto.InverterModelBlock; import org.openhab.binding.modbus.sunspec.internal.parser.InverterModelParser; -import org.openhab.io.transport.modbus.ModbusManager; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,8 +48,8 @@ public class InverterHandler extends AbstractSunSpecHandler { */ private final Logger logger = LoggerFactory.getLogger(InverterHandler.class); - public InverterHandler(Thing thing, ModbusManager managerRef) { - super(thing, managerRef); + public InverterHandler(Thing thing) { + super(thing); } /** diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/MeterHandler.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/MeterHandler.java index aba27a79140e9..1f5fc6c1ed353 100644 --- a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/MeterHandler.java +++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/MeterHandler.java @@ -19,7 +19,6 @@ import org.eclipse.smarthome.core.thing.Thing; import org.openhab.binding.modbus.sunspec.internal.dto.MeterModelBlock; import org.openhab.binding.modbus.sunspec.internal.parser.MeterModelParser; -import org.openhab.io.transport.modbus.ModbusManager; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,8 +42,8 @@ public class MeterHandler extends AbstractSunSpecHandler { */ private final Logger logger = LoggerFactory.getLogger(MeterHandler.class); - public MeterHandler(Thing thing, ModbusManager managerRef) { - super(thing, managerRef); + public MeterHandler(Thing thing) { + super(thing); } /** diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/discovery/internal/ModbusDiscoveryService.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/discovery/internal/ModbusDiscoveryService.java index c1c701a3a0e61..4738b7bca84f0 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/discovery/internal/ModbusDiscoveryService.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/discovery/internal/ModbusDiscoveryService.java @@ -15,7 +15,6 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService; import org.eclipse.smarthome.config.discovery.DiscoveryResult; @@ -116,7 +115,7 @@ protected void scanFinished() { * instances. They call back this method when a thing has been discovered */ @Override - protected void thingDiscovered(@NonNull DiscoveryResult discoveryResult) { + protected void thingDiscovered(DiscoveryResult discoveryResult) { super.thingDiscovered(discoveryResult); } diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusEndpointThingHandler.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusEndpointThingHandler.java index b91977e5f1066..b9d55342235c4 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusEndpointThingHandler.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusEndpointThingHandler.java @@ -12,14 +12,11 @@ */ package org.openhab.binding.modbus.handler; -import java.util.function.Supplier; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.common.registry.Identifiable; import org.eclipse.smarthome.core.thing.ThingUID; -import org.openhab.io.transport.modbus.ModbusManager; -import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; +import org.openhab.io.transport.modbus.ModbusCommunicationInterface; /** * Base interface for thing handlers of endpoint things @@ -31,13 +28,13 @@ public interface ModbusEndpointThingHandler extends Identifiable { /** - * Gets the {@link ModbusSlaveEndpoint} represented by the thing + * Gets the {@link ModbusCommunicationInterface} represented by the thing * - * Note that the endpoint can be null in case of incomplete initialization + * Note that this can be null in case of incomplete initialization * - * @return endpoint represented by this thing handler + * @return communication interface represented by this thing handler */ - public @Nullable ModbusSlaveEndpoint asSlaveEndpoint(); + public @Nullable ModbusCommunicationInterface getCommunicationInterface(); /** * Get Slave ID, also called as unit id, represented by the thing @@ -47,13 +44,6 @@ public interface ModbusEndpointThingHandler extends Identifiable { */ public int getSlaveId() throws EndpointNotInitializedException; - /** - * Get {@link ModbusManager} supplier - * - * @return reference to ModbusManager - */ - public Supplier getManagerRef(); - /** * Return true if auto discovery is enabled for this endpoint * diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusPollerThingHandlerImpl.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusPollerThingHandler.java similarity index 57% rename from bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusPollerThingHandlerImpl.java rename to bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusPollerThingHandler.java index 0bb1b09e4cb85..33d81d02058c6 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusPollerThingHandlerImpl.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/handler/ModbusPollerThingHandler.java @@ -10,17 +10,14 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.modbus.internal.handler; +package org.openhab.binding.modbus.handler; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Supplier; -import java.util.stream.Stream; import org.apache.commons.lang.StringUtils; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; @@ -29,36 +26,32 @@ import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.ThingStatusDetail; import org.eclipse.smarthome.core.thing.ThingStatusInfo; -import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.types.Command; -import org.openhab.binding.modbus.handler.EndpointNotInitializedException; -import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler; -import org.openhab.binding.modbus.internal.AtomicStampedKeyValue; +import org.openhab.binding.modbus.internal.AtomicStampedValue; import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal; import org.openhab.binding.modbus.internal.config.ModbusPollerConfiguration; -import org.openhab.io.transport.modbus.BasicModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.BasicPollTaskImpl; -import org.openhab.io.transport.modbus.BitArray; -import org.openhab.io.transport.modbus.ModbusManager; +import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler; +import org.openhab.io.transport.modbus.AsyncModbusFailure; +import org.openhab.io.transport.modbus.AsyncModbusReadResult; +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.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.PollTask; -import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The {@link ModbusPollerThingHandlerImpl} is responsible for polling Modbus slaves. Errors and data is delegated to + * The {@link ModbusPollerThingHandler} is responsible for polling Modbus slaves. Errors and data is delegated to * child thing handlers inheriting from {@link ModbusReadCallback} -- in practice: {@link ModbusDataThingHandler}. * * @author Sami Salonen - Initial contribution */ @NonNullByDefault -public class ModbusPollerThingHandlerImpl extends BaseBridgeHandler implements ModbusPollerThingHandler { +public class ModbusPollerThingHandler extends BaseBridgeHandler { /** * {@link ModbusReadCallback} that delegates all tasks forward. @@ -70,68 +63,45 @@ public class ModbusPollerThingHandlerImpl extends BaseBridgeHandler implements M * @author Sami Salonen * */ - private class ReadCallbackDelegator implements ModbusReadCallback { + private class ReadCallbackDelegator + implements ModbusReadCallback, ModbusFailureCallback { - private volatile @Nullable AtomicStampedKeyValue lastRegisters; - private volatile @Nullable AtomicStampedKeyValue lastCoils; - private volatile @Nullable AtomicStampedKeyValue lastError; + private volatile @Nullable AtomicStampedValue lastResult; - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { + public synchronized void handleResult(PollResult result) { // Ignore all incoming data and errors if configuration is not correct if (hasConfigurationError() || disposed) { return; } if (config.getCacheMillis() >= 0) { - AtomicStampedKeyValue lastRegisters = this.lastRegisters; - if (lastRegisters == null) { - this.lastRegisters = new AtomicStampedKeyValue<>(System.currentTimeMillis(), request, registers); + AtomicStampedValue localLastResult = this.lastResult; + if (localLastResult == null) { + this.lastResult = new AtomicStampedValue<>(System.currentTimeMillis(), result); } else { - lastRegisters.update(System.currentTimeMillis(), request, registers); + localLastResult.update(System.currentTimeMillis(), result); + this.lastResult = localLastResult; } } - logger.debug("Thing {} received registers {} for request {}", thing.getUID(), registers, request); - resetCommunicationError(); - childCallbacks.forEach(handler -> handler.onRegisters(request, registers)); + logger.debug("Thing {} received response {}", thing.getUID(), result); + notifyChildren(result); + if (result.failure != null) { + Exception error = result.failure.getCause(); + assert error != null; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + String.format("Error with read: %s: %s", error.getClass().getName(), error.getMessage())); + } else { + resetCommunicationError(); + } } @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray coils) { - // Ignore all incoming data and errors if configuration is not correct - if (hasConfigurationError() || disposed) { - return; - } - if (config.getCacheMillis() >= 0) { - AtomicStampedKeyValue lastCoils = this.lastCoils; - if (lastCoils == null) { - this.lastCoils = new AtomicStampedKeyValue<>(System.currentTimeMillis(), request, coils); - } else { - lastCoils.update(System.currentTimeMillis(), request, coils); - } - } - logger.debug("Thing {} received coils {} for request {}", thing.getUID(), coils, request); - resetCommunicationError(); - childCallbacks.forEach(handler -> handler.onBits(request, coils)); + public synchronized void handle(AsyncModbusReadResult result) { + handleResult(new PollResult(result)); } @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { - // Ignore all incoming data and errors if configuration is not correct - if (hasConfigurationError() || disposed) { - return; - } - if (config.getCacheMillis() >= 0) { - AtomicStampedKeyValue lastError = this.lastError; - if (lastError == null) { - this.lastError = new AtomicStampedKeyValue<>(System.currentTimeMillis(), request, error); - } else { - lastError.update(System.currentTimeMillis(), request, error); - } - } - logger.debug("Thing {} received error {} for request {}", thing.getUID(), error, request); - childCallbacks.forEach(handler -> handler.onError(request, error)); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - String.format("Error with read: %s: %s", error.getClass().getName(), error.getMessage())); + public synchronized void handle(AsyncModbusFailure failure) { + handleResult(new PollResult(failure)); } private void resetCommunicationError() { @@ -142,80 +112,41 @@ private void resetCommunicationError() { } } - private ThingUID getThingUID() { - return getThing().getUID(); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj == null) { - return false; - } - if (obj == this) { - return true; - } - if (obj.getClass() != getClass()) { - return false; - } - ReadCallbackDelegator rhs = (ReadCallbackDelegator) obj; - return getThingUID().equals(rhs.getThingUID()); - } - - @Override - public int hashCode() { - return getThingUID().hashCode(); - } - - @SuppressWarnings("unchecked") - private @Nullable AtomicStampedKeyValue getLastData() { - try { - return (AtomicStampedKeyValue) Stream - .of(lastRegisters, lastCoils, lastError).max(AtomicStampedKeyValue::compare).get(); - } catch (NullPointerException e) { - // max (latest) element is null -> all data are null - return null; - } - } - /** * Update children data if data is fresh enough * * @param oldestStamp oldest data that is still passed to children * @return whether data was updated. Data is not updated when it's too old or there's no data at all. */ + @SuppressWarnings("null") public boolean updateChildrenWithOldData(long oldestStamp) { - AtomicStampedKeyValue lastData = getLastData(); - if (lastData == null) { - return false; - } - AtomicStampedKeyValue atomicData = lastData - .copyIfStampAfter(oldestStamp); - if (atomicData == null) { - return false; - } - ModbusReadRequestBlueprint request = atomicData.getKey(); - logger.debug("Thing {} received data {} for request {}. Reusing cached data.", thing.getUID(), - atomicData.getValue(), request); - if (atomicData.getValue() instanceof ModbusRegisterArray) { - ModbusRegisterArray registers = (ModbusRegisterArray) atomicData.getValue(); - childCallbacks.forEach(handler -> handler.onRegisters(atomicData.getKey(), registers)); - } else if (atomicData.getValue() instanceof BitArray) { - BitArray coils = (BitArray) atomicData.getValue(); - childCallbacks.forEach(handler -> handler.onBits(request, coils)); - } else { - Exception error = (Exception) atomicData.getValue(); - childCallbacks.forEach(handler -> handler.onError(request, error)); - } - return true; + return Optional.ofNullable(this.lastResult).map(result -> result.copyIfStampAfter(oldestStamp)) + .map(result -> { + logger.debug("Thing {} reusing cached data: {}", thing.getUID(), result.getValue()); + notifyChildren(result.getValue()); + return true; + }).orElse(false); + } + + private void notifyChildren(PollResult pollResult) { + @Nullable + AsyncModbusReadResult result = pollResult.result; + @Nullable + AsyncModbusFailure failure = pollResult.failure; + childCallbacks.forEach(handler -> { + if (result != null) { + handler.onReadResult(result); + } else if (failure != null) { + handler.handleReadError(failure); + } + }); } /** * Rest data caches */ public void resetCache() { - lastRegisters = null; - lastCoils = null; - lastError = null; + lastResult = null; } } @@ -225,7 +156,7 @@ public void resetCache() { * @author Sami Salonen * */ - private static class ModbusPollerReadRequest extends BasicModbusReadRequestBlueprint { + private static class ModbusPollerReadRequest extends ModbusReadRequestBlueprint { private static ModbusReadFunctionCode getFunctionCode(@Nullable String type) { if (!ModbusBindingConstantsInternal.READ_FUNCTION_CODES.containsKey(type)) { @@ -246,21 +177,45 @@ public ModbusPollerReadRequest(ModbusPollerConfiguration config, } } - private final Logger logger = LoggerFactory.getLogger(ModbusPollerThingHandlerImpl.class); + /** + * Immutable data object to cache the results of a poll request + */ + private class PollResult { + + public final @Nullable AsyncModbusReadResult result; + public final @Nullable AsyncModbusFailure failure; + + PollResult(AsyncModbusReadResult result) { + this.result = result; + this.failure = null; + } + + PollResult(AsyncModbusFailure failure) { + this.result = null; + this.failure = failure; + } + + @Override + public String toString() { + return result == null ? String.format("PollResult(result=%s)", result) + : String.format("PollResult(failure=%s)", failure); + } + } + + private final Logger logger = LoggerFactory.getLogger(ModbusPollerThingHandler.class); - @NonNullByDefault({}) - private ModbusPollerConfiguration config; + private @NonNullByDefault({}) ModbusPollerConfiguration config; private long cacheMillis; private volatile @Nullable PollTask pollTask; - private Supplier managerRef; + private volatile @Nullable ModbusReadRequestBlueprint request; private volatile boolean disposed; - private volatile List childCallbacks = new CopyOnWriteArrayList<>(); + private volatile List childCallbacks = new CopyOnWriteArrayList<>(); + private @NonNullByDefault({}) ModbusCommunicationInterface comms; private ReadCallbackDelegator callbackDelegator = new ReadCallbackDelegator(); - public ModbusPollerThingHandlerImpl(Bridge bridge, Supplier managerRef) { + public ModbusPollerThingHandler(Bridge bridge) { super(bridge); - this.managerRef = managerRef; } @Override @@ -302,6 +257,7 @@ public synchronized void initialize() { updateStatus(ThingStatus.OFFLINE); } this.callbackDelegator.resetCache(); + comms = null; disposed = false; logger.trace("Initializing {} from status {}", this.getThing().getUID(), this.getThing().getStatus()); try { @@ -324,6 +280,7 @@ public synchronized void dispose() { disposed = true; unregisterPollTask(); this.callbackDelegator.resetCache(); + comms = null; } /** @@ -333,14 +290,17 @@ public synchronized void dispose() { */ public synchronized void unregisterPollTask() { logger.trace("unregisterPollTask()"); - if (pollTask == null || config == null) { + if (config == null) { return; } - logger.debug("Unregistering polling from ModbusManager"); - @NonNull - PollTask task = (@NonNull PollTask) pollTask; - managerRef.get().unregisterRegularPoll(task); - pollTask = null; + PollTask localPollTask = this.pollTask; + if (localPollTask != null) { + logger.debug("Unregistering polling from ModbusManager"); + comms.unregisterRegularPoll(localPollTask); + } + this.pollTask = null; + request = null; + comms = null; updateStatus(ThingStatus.OFFLINE); } @@ -366,25 +326,26 @@ private synchronized void registerPollTask() throws EndpointNotInitializedExcept logger.debug("No bridge handler available -- aborting init for {}", this); return; } - ModbusSlaveEndpoint endpoint = slaveEndpointThingHandler.asSlaveEndpoint(); - if (endpoint == null) { + ModbusCommunicationInterface localComms = slaveEndpointThingHandler.getCommunicationInterface(); + if (localComms == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, String.format( "Bridge '%s' not completely initialized", Optional.ofNullable(getBridge()).map(b -> b.getLabel()))); - logger.debug("Bridge not initialized fully (no endpoint) -- aborting init for {}", this); + logger.debug("Bridge not initialized fully (no communication interface) -- aborting init for {}", this); return; } + this.comms = localComms; - BasicModbusReadRequestBlueprint request = new ModbusPollerReadRequest(config, slaveEndpointThingHandler); - @NonNull - PollTask task = new BasicPollTaskImpl(endpoint, request, callbackDelegator); - pollTask = task; + ModbusReadRequestBlueprint localRequest = new ModbusPollerReadRequest(config, slaveEndpointThingHandler); + this.request = localRequest; if (config.getRefresh() <= 0L) { logger.debug("Not registering polling with ModbusManager since refresh disabled"); updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Not polling"); } else { logger.debug("Registering polling with ModbusManager"); - managerRef.get().registerRegularPoll(task, config.getRefresh(), 0); + pollTask = localComms.registerRegularPoll(localRequest, config.getRefresh(), 0, callbackDelegator, + callbackDelegator); + assert pollTask != null; updateStatus(ThingStatus.ONLINE); } } @@ -404,27 +365,37 @@ public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { @Override public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) { - if (childHandler instanceof ModbusReadCallback) { - this.childCallbacks.add((ModbusReadCallback) childHandler); + if (childHandler instanceof ModbusDataThingHandler) { + this.childCallbacks.add((ModbusDataThingHandler) childHandler); } } @SuppressWarnings("unlikely-arg-type") @Override public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { - if (childHandler instanceof ModbusReadCallback) { + if (childHandler instanceof ModbusDataThingHandler) { this.childCallbacks.remove(childHandler); } } - @Override - public Supplier getManagerRef() { - return managerRef; + /** + * Return {@link ModbusReadRequestBlueprint} represented by this thing. + * + * Note that request might be null in case initialization is not complete. + * + * @return modbus request represented by this poller + */ + public @Nullable ModbusReadRequestBlueprint getRequest() { + return request; } - @Override - public @Nullable PollTask getPollTask() { - return pollTask; + /** + * Get communication interface associated with this poller + * + * @return + */ + public ModbusCommunicationInterface getCommunicationInterface() { + return comms; } /** @@ -432,10 +403,9 @@ public Supplier getManagerRef() { * * If data or error was just recently received (i.e. cache is fresh), return the cached response. */ - @Override public void refresh() { - PollTask pollTask = this.pollTask; - if (pollTask == null) { + ModbusReadRequestBlueprint localRequest = this.request; + if (localRequest == null) { return; } @@ -450,7 +420,10 @@ public void refresh() { // cache expired, poll new data logger.debug("Poller {} received refresh() but the cache is not applicable. Polling new data", getThing().getUID()); - managerRef.get().submitOneTimePoll(pollTask); + ModbusCommunicationInterface localComms = comms; + if (localComms != null) { + localComms.submitOneTimePoll(localRequest, callbackDelegator, callbackDelegator); + } } } } diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/AtomicStampedKeyValue.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/AtomicStampedValue.java similarity index 73% rename from bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/AtomicStampedKeyValue.java rename to bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/AtomicStampedValue.java index cf01a902d2db7..97eec8052a8ce 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/AtomicStampedKeyValue.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/AtomicStampedValue.java @@ -18,38 +18,33 @@ import org.eclipse.jdt.annotation.Nullable; /** - * Timestamped key-value pair that can be updated atomically + * Timestamp-value pair that can be updated atomically * * @author Sami Salonen - Initial contribution * - * @param type of the key * @param type of the value */ @NonNullByDefault -public class AtomicStampedKeyValue implements Cloneable { +public class AtomicStampedValue implements Cloneable { private long stamp; - private K key; private V value; - private AtomicStampedKeyValue(AtomicStampedKeyValue copy) { - this(copy.stamp, copy.key, copy.value); + private AtomicStampedValue(AtomicStampedValue copy) { + this(copy.stamp, copy.value); } /** * Construct new stamped key-value pair * * @param stamp stamp for the data - * @param key key for the data * @param value value for the data * * @throws NullPointerException when key or value is null */ - public AtomicStampedKeyValue(long stamp, K key, V value) { - Objects.requireNonNull(key, "key should not be null!"); + public AtomicStampedValue(long stamp, V value) { Objects.requireNonNull(value, "value should not be null!"); this.stamp = stamp; - this.key = key; this.value = value; } @@ -57,16 +52,13 @@ public AtomicStampedKeyValue(long stamp, K key, V value) { * Update data in this instance atomically * * @param stamp stamp for the data - * @param key key for the data * @param value value for the data * - * @throws NullPointerException when key or value is null + * @throws NullPointerException when value is null */ - public synchronized void update(long stamp, K key, V value) { - Objects.requireNonNull(key, "key should not be null!"); + public synchronized void update(long stamp, V value) { Objects.requireNonNull(value, "value should not be null!"); this.stamp = stamp; - this.key = key; this.value = value; } @@ -77,8 +69,8 @@ public synchronized void update(long stamp, K key, V value) { * @throws CloneNotSupportedException */ @SuppressWarnings("unchecked") - public synchronized AtomicStampedKeyValue copy() { - return (AtomicStampedKeyValue) this.clone(); + public synchronized AtomicStampedValue copy() { + return (AtomicStampedValue) this.clone(); } /** @@ -100,9 +92,9 @@ protected synchronized Object clone() { * @param stampMin * @return null, if the stamp of this instance is before stampMin. Otherwise return the data copied */ - public synchronized @Nullable AtomicStampedKeyValue copyIfStampAfter(long stampMin) { + public synchronized @Nullable AtomicStampedValue copyIfStampAfter(long stampMin) { if (stampMin <= this.stamp) { - return new AtomicStampedKeyValue<>(this); + return new AtomicStampedValue<>(this); } else { return null; } @@ -115,13 +107,6 @@ public long getStamp() { return stamp; } - /** - * Get key - */ - public K getKey() { - return key; - } - /** * Get value */ @@ -139,8 +124,8 @@ public V getValue() { * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater * than the second. */ - public static int compare(@SuppressWarnings("rawtypes") @Nullable AtomicStampedKeyValue x, - @SuppressWarnings("rawtypes") @Nullable AtomicStampedKeyValue y) { + public static int compare(@SuppressWarnings("rawtypes") @Nullable AtomicStampedValue x, + @SuppressWarnings("rawtypes") @Nullable AtomicStampedValue y) { if (x == null) { return -1; } else if (y == null) { diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ModbusHandlerFactory.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ModbusHandlerFactory.java index 4bb0dbe07e088..8b4416182ef35 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ModbusHandlerFactory.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/ModbusHandlerFactory.java @@ -25,8 +25,8 @@ import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.openhab.binding.modbus.handler.ModbusPollerThingHandler; import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler; -import org.openhab.binding.modbus.internal.handler.ModbusPollerThingHandlerImpl; import org.openhab.binding.modbus.internal.handler.ModbusSerialThingHandler; import org.openhab.binding.modbus.internal.handler.ModbusTcpThingHandler; import org.openhab.io.transport.modbus.ModbusManager; @@ -47,8 +47,7 @@ public class ModbusHandlerFactory extends BaseThingHandlerFactory { private final Logger logger = LoggerFactory.getLogger(ModbusHandlerFactory.class); - @NonNullByDefault({}) - private ModbusManager manager; + private @NonNullByDefault({}) ModbusManager manager; private static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet<>(); static { @@ -68,13 +67,13 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (thingTypeUID.equals(THING_TYPE_MODBUS_TCP)) { logger.debug("createHandler Modbus tcp"); - return new ModbusTcpThingHandler((Bridge) thing, () -> manager); + return new ModbusTcpThingHandler((Bridge) thing, manager); } else if (thingTypeUID.equals(THING_TYPE_MODBUS_SERIAL)) { logger.debug("createHandler Modbus serial"); - return new ModbusSerialThingHandler((Bridge) thing, () -> manager); + return new ModbusSerialThingHandler((Bridge) thing, manager); } else if (thingTypeUID.equals(THING_TYPE_MODBUS_POLLER)) { logger.debug("createHandler Modbus poller"); - return new ModbusPollerThingHandlerImpl((Bridge) thing, () -> manager); + return new ModbusPollerThingHandler((Bridge) thing); } else if (thingTypeUID.equals(THING_TYPE_MODBUS_DATA)) { logger.debug("createHandler data"); return new ModbusDataThingHandler(thing); diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/Transformation.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/Transformation.java index 94137115566a3..fbd48c2ae1206 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/Transformation.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/Transformation.java @@ -73,12 +73,9 @@ public class Transformation { toStringStyle.setUseShortClassName(true); } - @Nullable - private final String transformation; - @Nullable - private final String transformationServiceName; - @Nullable - private final String transformationServiceParam; + private final @Nullable String transformation; + private final @Nullable String transformationServiceName; + private final @Nullable String transformationServiceParam; /** * diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusDataConfiguration.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusDataConfiguration.java index d0326113ca8d5..f9c9ef67f2d3c 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusDataConfiguration.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusDataConfiguration.java @@ -24,20 +24,13 @@ @NonNullByDefault public class ModbusDataConfiguration { - @Nullable - private String readStart; - @Nullable - private String readTransform; - @Nullable - private String readValueType; - @Nullable - private String writeStart; - @Nullable - private String writeType; - @Nullable - private String writeTransform; - @Nullable - private String writeValueType; + private @Nullable String readStart; + private @Nullable String readTransform; + private @Nullable String readValueType; + private @Nullable String writeStart; + private @Nullable String writeType; + private @Nullable String writeTransform; + private @Nullable String writeValueType; private boolean writeMultipleEvenWithSingleRegisterOrCoil; private int writeMaxTries = 3; // backwards compatibility and tests private long updateUnchangedValuesEveryMillis = 1000L; diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusPollerConfiguration.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusPollerConfiguration.java index efe2ef444e72d..f121236316608 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusPollerConfiguration.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusPollerConfiguration.java @@ -26,8 +26,7 @@ public class ModbusPollerConfiguration { private long refresh; private int start; private int length; - @Nullable - private String type; + private @Nullable String type; private int maxTries = 3;// backwards compatibility and tests private long cacheMillis = 50L; diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusSerialConfiguration.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusSerialConfiguration.java index 016d569da01c0..03c6b395df73f 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusSerialConfiguration.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusSerialConfiguration.java @@ -23,23 +23,17 @@ */ @NonNullByDefault public class ModbusSerialConfiguration { - @Nullable - private String port; + private @Nullable String port; private int id; private int baud; - @Nullable - private String stopBits; - @Nullable - private String parity; + private @Nullable String stopBits; + private @Nullable String parity; private int dataBits; - @Nullable - private String encoding; + private @Nullable String encoding; private boolean echo; private int receiveTimeoutMillis; - @Nullable - private String flowControlIn; - @Nullable - private String flowControlOut; + private @Nullable String flowControlIn; + private @Nullable String flowControlOut; private int timeBetweenTransactionsMillis; private int connectMaxTries; private int connectTimeoutMillis; diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusTcpConfiguration.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusTcpConfiguration.java index 1c1f0841f79fc..b4314f45e7aa5 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusTcpConfiguration.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/config/ModbusTcpConfiguration.java @@ -23,8 +23,7 @@ */ @NonNullByDefault public class ModbusTcpConfiguration { - @Nullable - private String host; + private @Nullable String host; private int port; private int id; private int timeBetweenTransactionsMillis; diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/AbstractModbusEndpointThingHandler.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/AbstractModbusEndpointThingHandler.java index 6778b6b6fc737..48d437d59a89d 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/AbstractModbusEndpointThingHandler.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/AbstractModbusEndpointThingHandler.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.modbus.internal.handler; -import java.util.function.Supplier; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; @@ -24,8 +22,8 @@ import org.eclipse.smarthome.core.types.Command; import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler; import org.openhab.binding.modbus.internal.ModbusConfigurationException; +import org.openhab.io.transport.modbus.ModbusCommunicationInterface; import org.openhab.io.transport.modbus.ModbusManager; -import org.openhab.io.transport.modbus.ModbusManagerListener; import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration; import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; import org.slf4j.Logger; @@ -41,20 +39,18 @@ */ @NonNullByDefault public abstract class AbstractModbusEndpointThingHandler extends BaseBridgeHandler - implements ModbusManagerListener, ModbusEndpointThingHandler { + implements ModbusEndpointThingHandler { - @Nullable - protected volatile C config; - @Nullable - protected volatile E endpoint; - protected Supplier managerRef; - @Nullable - protected volatile EndpointPoolConfiguration poolConfiguration; + protected volatile @Nullable C config; + protected volatile @Nullable E endpoint; + protected ModbusManager modbusManager; + protected volatile @Nullable EndpointPoolConfiguration poolConfiguration; private final Logger logger = LoggerFactory.getLogger(AbstractModbusEndpointThingHandler.class); + private @NonNullByDefault({}) ModbusCommunicationInterface comms; - public AbstractModbusEndpointThingHandler(Bridge bridge, Supplier managerRef) { + public AbstractModbusEndpointThingHandler(Bridge bridge, ModbusManager modbusManager) { super(bridge); - this.managerRef = managerRef; + this.modbusManager = modbusManager; } @Override @@ -75,11 +71,15 @@ public void initialize() { @Nullable E endpoint = this.endpoint; if (endpoint == null) { - throw new IllegalArgumentException("endpoint null after configuration!"); + throw new IllegalStateException("endpoint null after configuration!"); + } + try { + comms = modbusManager.newModbusCommunicationInterface(endpoint, poolConfiguration); + updateStatus(ThingStatus.ONLINE); + } catch (IllegalArgumentException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + formatConflictingParameterError()); } - managerRef.get().addListener(this); - managerRef.get().setEndpointPoolConfiguration(endpoint, poolConfiguration); - updateStatus(ThingStatus.ONLINE); } catch (ModbusConfigurationException e) { logger.debug("Exception during initialization", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( @@ -92,33 +92,26 @@ public void initialize() { @Override public void dispose() { - managerRef.get().removeListener(this); - } - - @Override - public @Nullable ModbusSlaveEndpoint asSlaveEndpoint() { - return endpoint; + try { + ModbusCommunicationInterface localComms = comms; + if (localComms != null) { + localComms.close(); + } + } catch (Exception e) { + logger.warn("Error closing modbus communication interface", e); + } finally { + comms = null; + } } @Override - public Supplier getManagerRef() { - return managerRef; + public @Nullable ModbusCommunicationInterface getCommunicationInterface() { + return comms; } - @Override - public void onEndpointPoolConfigurationSet(ModbusSlaveEndpoint otherEndpoint, - @Nullable EndpointPoolConfiguration otherPoolConfiguration) { - synchronized (this) { - if (endpoint == null) { - return; - } - EndpointPoolConfiguration poolConfiguration = this.poolConfiguration; - if (poolConfiguration != null && otherEndpoint.equals(this.endpoint) - && !poolConfiguration.equals(otherPoolConfiguration)) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - formatConflictingParameterError(otherPoolConfiguration)); - } - } + @Nullable + public E getEndpoint() { + return endpoint; } @Override @@ -132,9 +125,6 @@ public void onEndpointPoolConfigurationSet(ModbusSlaveEndpoint otherEndpoint, /** * Format error message in case some other endpoint has been configured with different * {@link EndpointPoolConfiguration} - * - * @param otherPoolConfig - * @return */ - protected abstract String formatConflictingParameterError(@Nullable EndpointPoolConfiguration otherPoolConfig); + protected abstract String formatConflictingParameterError(); } diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusDataThingHandler.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusDataThingHandler.java index f70444ebce4f1..02328229d55a2 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusDataThingHandler.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusDataThingHandler.java @@ -18,16 +18,15 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang.StringUtils; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.items.ContactItem; @@ -56,29 +55,27 @@ import org.eclipse.smarthome.core.types.UnDefType; import org.openhab.binding.modbus.handler.EndpointNotInitializedException; import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler; +import org.openhab.binding.modbus.handler.ModbusPollerThingHandler; import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal; import org.openhab.binding.modbus.internal.ModbusConfigurationException; import org.openhab.binding.modbus.internal.Transformation; import org.openhab.binding.modbus.internal.config.ModbusDataConfiguration; -import org.openhab.io.transport.modbus.BasicModbusWriteCoilRequestBlueprint; -import org.openhab.io.transport.modbus.BasicModbusWriteRegisterRequestBlueprint; -import org.openhab.io.transport.modbus.BasicWriteTask; +import org.openhab.io.transport.modbus.AsyncModbusFailure; +import org.openhab.io.transport.modbus.AsyncModbusReadResult; +import org.openhab.io.transport.modbus.AsyncModbusWriteResult; import org.openhab.io.transport.modbus.BitArray; import org.openhab.io.transport.modbus.ModbusBitUtilities; -import org.openhab.io.transport.modbus.ModbusConnectionException; +import org.openhab.io.transport.modbus.ModbusCommunicationInterface; import org.openhab.io.transport.modbus.ModbusConstants; import org.openhab.io.transport.modbus.ModbusConstants.ValueType; -import org.openhab.io.transport.modbus.ModbusManager; -import org.openhab.io.transport.modbus.ModbusReadCallback; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; import org.openhab.io.transport.modbus.ModbusRegisterArray; -import org.openhab.io.transport.modbus.ModbusResponse; -import org.openhab.io.transport.modbus.ModbusTransportException; -import org.openhab.io.transport.modbus.ModbusWriteCallback; +import org.openhab.io.transport.modbus.ModbusWriteCoilRequestBlueprint; +import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint; import org.openhab.io.transport.modbus.ModbusWriteRequestBlueprint; -import org.openhab.io.transport.modbus.PollTask; -import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; +import org.openhab.io.transport.modbus.exception.ModbusConnectionException; +import org.openhab.io.transport.modbus.exception.ModbusTransportException; import org.openhab.io.transport.modbus.json.WriteRequestJsonUtilities; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; @@ -97,7 +94,7 @@ * @author Sami Salonen - Initial contribution */ @NonNullByDefault -public class ModbusDataThingHandler extends BaseThingHandler implements ModbusReadCallback, ModbusWriteCallback { +public class ModbusDataThingHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(ModbusDataThingHandler.class); @@ -138,10 +135,10 @@ public class ModbusDataThingHandler extends BaseThingHandler implements ModbusRe private volatile @Nullable Integer writeStart; private volatile int pollStart; private volatile int slaveId; + private volatile @Nullable ModbusReadFunctionCode functionCode; + private volatile @Nullable ModbusReadRequestBlueprint readRequest; private volatile long updateUnchangedValuesEveryMillis; - private volatile @Nullable ModbusSlaveEndpoint slaveEndpoint; - private volatile @Nullable ModbusManager manager; - private volatile @Nullable PollTask pollTask; + private volatile @NonNullByDefault({}) ModbusCommunicationInterface comms; private volatile boolean isWriteEnabled; private volatile boolean isReadEnabled; private volatile boolean writeParametersHavingTransformationOnly; @@ -165,8 +162,7 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { logger.trace("Thing {} '{}' received command '{}' to channel '{}'", getThing().getUID(), getThing().getLabel(), command, channelUID); ModbusDataConfiguration config = this.config; - ModbusManager manager = this.manager; - if (config == null || manager == null) { + if (config == null) { return; } @@ -220,14 +216,12 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { ModbusWriteRequestBlueprint request = requestFromCommand(channelUID, command, config, transformedCommand.get(), writeStart); - ModbusSlaveEndpoint slaveEndpoint = this.slaveEndpoint; - if (request == null || slaveEndpoint == null) { + if (request == null) { return; } - BasicWriteTask writeTask = new BasicWriteTask(slaveEndpoint, request, this); - logger.trace("Submitting write task: {}", writeTask); - manager.submitOneTimeWrite(writeTask); + logger.trace("Submitting write task {} to endpoint {}", request, comms.getEndpoint()); + comms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError); } /** @@ -287,7 +281,7 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { return null; } boolean data = commandAsBoolean.get(); - request = new BasicModbusWriteCoilRequestBlueprint(slaveId, writeStart, data, writeMultiple, + request = new ModbusWriteCoilRequestBlueprint(slaveId, writeStart, data, writeMultiple, config.getWriteMaxTries()); } else if (writeType.equals(WRITE_TYPE_HOLDING)) { ValueType writeValueType = this.writeValueType; @@ -299,7 +293,7 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { } ModbusRegisterArray data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType); writeMultiple = writeMultiple || data.size() > 1; - request = new BasicModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple, + request = new ModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple, config.getWriteMaxTries()); } else { // Should not happen! This method is not called in case configuration errors and writeType is validated @@ -313,9 +307,8 @@ public synchronized void handleCommand(ChannelUID channelUID, Command command) { } private void processJsonTransform(Command command, String transformOutput) { - ModbusSlaveEndpoint slaveEndpoint = this.slaveEndpoint; - ModbusManager manager = this.manager; - if (slaveEndpoint == null || manager == null) { + ModbusCommunicationInterface localComms = this.comms; + if (localComms == null) { return; } Collection requests; @@ -328,9 +321,10 @@ private void processJsonTransform(Command command, String transformOutput) { return; } - requests.stream().map(request -> new BasicWriteTask(slaveEndpoint, request, this)).forEach(writeTask -> { - logger.trace("Submitting write task: {} (based from transformation {})", writeTask, transformOutput); - manager.submitOneTimeWrite(writeTask); + requests.stream().forEach(request -> { + logger.trace("Submitting write request: {} to endpoint {} (based from transformation {})", request, + localComms.getEndpoint(), transformOutput); + localComms.submitOneTimeWrite(request, this::onWriteResponse, this::handleWriteError); }); } @@ -340,8 +334,8 @@ public synchronized void initialize() { // Long running initialization should be done asynchronously in background. try { logger.trace("initialize() of thing {} '{}' starting", thing.getUID(), thing.getLabel()); - config = getConfigAs(ModbusDataConfiguration.class); - updateUnchangedValuesEveryMillis = config.getUpdateUnchangedValuesEveryMillis(); + ModbusDataConfiguration localConfig = config = getConfigAs(ModbusDataConfiguration.class); + updateUnchangedValuesEveryMillis = localConfig.getUpdateUnchangedValuesEveryMillis(); Bridge bridge = getBridge(); if (bridge == null) { logger.debug("Thing {} '{}' has no bridge", getThing().getUID(), getThing().getLabel()); @@ -359,29 +353,30 @@ public synchronized void initialize() { // Write-only thing, parent is endpoint ModbusEndpointThingHandler endpointHandler = (ModbusEndpointThingHandler) bridgeHandler; slaveId = endpointHandler.getSlaveId(); - slaveEndpoint = endpointHandler.asSlaveEndpoint(); - manager = endpointHandler.getManagerRef().get(); + comms = endpointHandler.getCommunicationInterface(); childOfEndpoint = true; - pollTask = null; + functionCode = null; + readRequest = null; } else { - pollerHandler = (ModbusPollerThingHandler) bridgeHandler; - PollTask pollTask = pollerHandler.getPollTask(); - this.pollTask = pollTask; - if (pollTask == null) { - logger.debug("Poller {} '{}' has no poll task -- configuration is changing?", bridge.getUID(), + ModbusPollerThingHandler localPollerHandler = (ModbusPollerThingHandler) bridgeHandler; + pollerHandler = localPollerHandler; + ModbusReadRequestBlueprint localReadRequest = localPollerHandler.getRequest(); + if (localReadRequest == null) { + logger.debug("Poller {} '{}' has no read request -- configuration is changing?", bridge.getUID(), bridge.getLabel()); updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, String.format("Poller %s '%s' has no poll task", bridge.getUID(), bridge.getLabel())); return; } - slaveId = pollTask.getRequest().getUnitID(); - slaveEndpoint = pollTask.getEndpoint(); - manager = pollerHandler.getManagerRef().get(); - pollStart = pollTask.getRequest().getReference(); + readRequest = localReadRequest; + slaveId = localReadRequest.getUnitID(); + functionCode = localReadRequest.getFunctionCode(); + comms = localPollerHandler.getCommunicationInterface(); + pollStart = localReadRequest.getReference(); childOfEndpoint = false; } - validateAndParseReadParameters(); - validateAndParseWriteParameters(); + validateAndParseReadParameters(localConfig); + validateAndParseWriteParameters(localConfig); validateMustReadOrWrite(); updateStatusIfChanged(ThingStatus.ONLINE); @@ -406,9 +401,9 @@ public synchronized void dispose() { writeStart = null; pollStart = 0; slaveId = 0; - slaveEndpoint = null; - manager = null; - pollTask = null; + comms = null; + functionCode = null; + readRequest = null; isWriteEnabled = false; isReadEnabled = false; writeParametersHavingTransformationOnly = false; @@ -440,17 +435,14 @@ private void validateMustReadOrWrite() throws ModbusConfigurationException { } } - private void validateAndParseReadParameters() throws ModbusConfigurationException { - ModbusDataConfiguration config = this.config; - Objects.requireNonNull(config); - @SuppressWarnings("null") - ModbusReadFunctionCode functionCode = pollTask == null ? null : pollTask.getRequest().getFunctionCode(); + private void validateAndParseReadParameters(ModbusDataConfiguration config) throws ModbusConfigurationException { + ModbusReadFunctionCode functionCode = this.functionCode; boolean readingDiscreteOrCoil = functionCode == ModbusReadFunctionCode.READ_COILS || functionCode == ModbusReadFunctionCode.READ_INPUT_DISCRETES; boolean readStartMissing = StringUtils.isBlank(config.getReadStart()); boolean readValueTypeMissing = StringUtils.isBlank(config.getReadValueType()); - if (childOfEndpoint && pollTask == null) { + if (childOfEndpoint && readRequest == null) { if (!readStartMissing || !readValueTypeMissing) { String errmsg = String.format( "Thing %s readStart=%s, and readValueType=%s were specified even though the data thing is child of endpoint (that is, write-only)!", @@ -493,7 +485,12 @@ private void validateAndParseReadParameters() throws ModbusConfigurationExceptio } if (isReadEnabled) { - String[] readParts = config.getReadStart().split("\\.", 2); + String readStart = config.getReadStart(); + if (readStart == null) { + throw new ModbusConfigurationException( + String.format("Thing %s invalid readStart: %s", getThing().getUID(), config.getReadStart())); + } + String[] readParts = readStart.split("\\.", 2); try { readIndex = Optional.of(Integer.parseInt(readParts[0])); if (readParts.length == 2) { @@ -508,11 +505,10 @@ private void validateAndParseReadParameters() throws ModbusConfigurationExceptio } } readTransformation = new Transformation(config.getReadTransform()); - - validateReadIndex(pollTask); + validateReadIndex(); } - private void validateAndParseWriteParameters() throws ModbusConfigurationException { + private void validateAndParseWriteParameters(ModbusDataConfiguration config) throws ModbusConfigurationException { boolean writeTypeMissing = StringUtils.isBlank(config.getWriteType()); boolean writeStartMissing = StringUtils.isBlank(config.getWriteStart()); boolean writeValueTypeMissing = StringUtils.isBlank(config.getWriteValueType()); @@ -542,26 +538,27 @@ private void validateAndParseWriteParameters() throws ModbusConfigurationExcepti WRITE_TYPE_HOLDING, WRITE_TYPE_COIL); throw new ModbusConfigurationException(errmsg); } + final ValueType localWriteValueType; if (writeParametersHavingTransformationOnly) { // Placeholder for further checks - writeValueType = ModbusConstants.ValueType.INT16; + localWriteValueType = writeValueType = ModbusConstants.ValueType.INT16; } else if (writingCoil && writeValueTypeMissing) { - writeValueType = ModbusConstants.ValueType.BIT; + localWriteValueType = writeValueType = ModbusConstants.ValueType.BIT; } else { try { - writeValueType = ValueType.fromConfigValue(config.getWriteValueType()); + localWriteValueType = writeValueType = ValueType.fromConfigValue(config.getWriteValueType()); } catch (IllegalArgumentException e) { String errmsg = String.format("Invalid writeValueType=%s!", config.getWriteValueType()); throw new ModbusConfigurationException(errmsg); } } - if (writingCoil && !ModbusConstants.ValueType.BIT.equals(writeValueType)) { + if (writingCoil && !ModbusConstants.ValueType.BIT.equals(localWriteValueType)) { String errmsg = String.format( "Invalid writeValueType: Only writeValueType='%s' (or undefined) supported with coils. Value type was: %s", ModbusConstants.ValueType.BIT, config.getWriteValueType()); throw new ModbusConfigurationException(errmsg); - } else if (!writingCoil && writeValueType.getBits() < 16) { + } else if (!writingCoil && localWriteValueType.getBits() < 16) { // trying to write holding registers with < 16 bit value types. Not supported String errmsg = String.format( "Invalid writeValueType: Only writeValueType with larger or equal to 16 bits are supported holding registers. Value type was: %s", @@ -571,7 +568,13 @@ private void validateAndParseWriteParameters() throws ModbusConfigurationExcepti try { if (!writeParametersHavingTransformationOnly) { - writeStart = Integer.parseInt(config.getWriteStart().trim()); + String localWriteStart = config.getWriteStart(); + if (localWriteStart == null) { + String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(), + config.getWriteStart()); + throw new ModbusConfigurationException(errmsg); + } + writeStart = Integer.parseInt(localWriteStart.trim()); } } catch (IllegalArgumentException e) { String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(), @@ -583,14 +586,18 @@ private void validateAndParseWriteParameters() throws ModbusConfigurationExcepti } } - private void validateReadIndex(@Nullable PollTask pollTask) throws ModbusConfigurationException { - if (!readIndex.isPresent() || pollTask == null) { + private void validateReadIndex() throws ModbusConfigurationException { + @Nullable + ModbusReadRequestBlueprint readRequest = this.readRequest; + ValueType readValueType = this.readValueType; + if (!readIndex.isPresent() || readRequest == null) { return; } + assert readValueType != null; // bits represented by the value type, e.g. int32 -> 32 int valueTypeBitCount = readValueType.getBits(); int dataElementBits; - switch (pollTask.getRequest().getFunctionCode()) { + switch (readRequest.getFunctionCode()) { case READ_INPUT_REGISTERS: case READ_MULTIPLE_REGISTERS: dataElementBits = 16; @@ -600,7 +607,7 @@ private void validateReadIndex(@Nullable PollTask pollTask) throws ModbusConfigu dataElementBits = 1; break; default: - throw new IllegalStateException(pollTask.getRequest().getFunctionCode().toString()); + throw new IllegalStateException(readRequest.getFunctionCode().toString()); } boolean bitQuery = dataElementBits == 1; @@ -623,8 +630,8 @@ private void validateReadIndex(@Nullable PollTask pollTask) throws ModbusConfigu } // Determine bit positions polled, both start and end inclusive - int pollStartBitIndex = pollTask.getRequest().getReference() * dataElementBits; - int pollEndBitIndex = pollStartBitIndex + pollTask.getRequest().getDataLength() * dataElementBits; + int pollStartBitIndex = readRequest.getReference() * dataElementBits; + int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * dataElementBits; // Determine bit positions read, both start and end inclusive int readStartBitIndex = readIndex.get() * dataElementBits + readSubIndex.orElse(0) * valueTypeBitCount; @@ -651,8 +658,20 @@ private boolean containsOpenClosed(List> acceptedDataType }); } - @Override - public synchronized void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { + public synchronized void onReadResult(AsyncModbusReadResult result) { + result.getRegisters().ifPresent(registers -> onRegisters(result.getRequest(), registers)); + result.getBits().ifPresent(bits -> onBits(result.getRequest(), bits)); + } + + public synchronized void handleReadError(AsyncModbusFailure failure) { + onError(failure.getRequest(), failure.getCause()); + } + + public synchronized void handleWriteError(AsyncModbusFailure failure) { + onError(failure.getRequest(), failure.getCause()); + } + + private synchronized void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { if (hasConfigurationError()) { return; } else if (!isReadEnabled) { @@ -693,8 +712,7 @@ public synchronized void onRegisters(ModbusReadRequestBlueprint request, ModbusR boolValue, registers, request); } - @Override - public synchronized void onBits(ModbusReadRequestBlueprint request, BitArray bits) { + private synchronized void onBits(ModbusReadRequestBlueprint request, BitArray bits) { if (hasConfigurationError()) { return; } else if (!isReadEnabled) { @@ -708,8 +726,7 @@ public synchronized void onBits(ModbusReadRequestBlueprint request, BitArray bit thing.getUID(), values, readValueType, readIndex, numericState, boolValue, bits, request); } - @Override - public synchronized void onError(ModbusReadRequestBlueprint request, Exception error) { + private synchronized void onError(ModbusReadRequestBlueprint request, Exception error) { if (hasConfigurationError()) { return; } else if (!isReadEnabled) { @@ -727,7 +744,7 @@ public synchronized void onError(ModbusReadRequestBlueprint request, Exception e getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(), error.getMessage(), error); } - Map<@NonNull ChannelUID, @NonNull State> states = new HashMap<>(); + Map states = new HashMap<>(); ChannelUID lastReadErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_READ_ERROR); if (isLinked(lastReadErrorUID)) { states.put(lastReadErrorUID, new DateTimeType()); @@ -745,8 +762,7 @@ public synchronized void onError(ModbusReadRequestBlueprint request, Exception e } } - @Override - public synchronized void onError(ModbusWriteRequestBlueprint request, Exception error) { + private synchronized void onError(ModbusWriteRequestBlueprint request, Exception error) { if (hasConfigurationError()) { return; } else if (!isWriteEnabled) { @@ -764,7 +780,7 @@ public synchronized void onError(ModbusWriteRequestBlueprint request, Exception getThing().getUID(), getThing().getLabel(), error.getClass().getName(), error.toString(), error.getMessage(), error); } - Map<@NonNull ChannelUID, @NonNull State> states = new HashMap<>(); + Map states = new HashMap<>(); ChannelUID lastWriteErrorUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_ERROR); if (isLinked(lastWriteErrorUID)) { states.put(lastWriteErrorUID, new DateTimeType()); @@ -782,14 +798,13 @@ public synchronized void onError(ModbusWriteRequestBlueprint request, Exception } } - @Override - public synchronized void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response) { + public synchronized void onWriteResponse(AsyncModbusWriteResult result) { if (hasConfigurationError()) { return; } else if (!isWriteEnabled) { return; } - logger.debug("Successful write, matching request {}", request); + logger.debug("Successful write, matching request {}", result.getRequest()); updateStatusIfChanged(ThingStatus.ONLINE); ChannelUID lastWriteSuccessUID = getChannelUID(ModbusBindingConstantsInternal.CHANNEL_LAST_WRITE_SUCCESS); if (isLinked(lastWriteSuccessUID)) { @@ -805,7 +820,13 @@ public synchronized void onWriteResponse(ModbusWriteRequestBlueprint request, Mo * @return updated channel data */ private Map processUpdatedValue(State numericState, boolean boolValue) { - Map<@NonNull ChannelUID, @NonNull State> states = new HashMap<>(); + Transformation localReadTransformation = readTransformation; + if (localReadTransformation == null) { + // We should always have transformation available if thing is initalized properly + logger.trace("No transformation available, aborting processUpdatedValue"); + return Collections.emptyMap(); + } + Map states = new HashMap<>(); CHANNEL_ID_TO_ACCEPTED_TYPES.keySet().stream().forEach(channelId -> { ChannelUID channelUID = getChannelUID(channelId); if (!isLinked(channelUID)) { @@ -826,7 +847,7 @@ private Map processUpdatedValue(State numericState, boolean b } State transformedState; - if (readTransformation.isIdentityTransform()) { + if (localReadTransformation.isIdentityTransform()) { if (boolLikeState != null) { // A bit of smartness for ON/OFF and OPEN/CLOSED with boolean like items transformedState = boolLikeState; @@ -834,11 +855,12 @@ private Map processUpdatedValue(State numericState, boolean b // Numeric states always go through transformation. This allows value of 17.5 to be // converted to // 17.5% with percent types (instead of raising error) - transformedState = readTransformation.transformState(bundleContext, acceptedDataTypes, + transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes, numericState); } } else { - transformedState = readTransformation.transformState(bundleContext, acceptedDataTypes, numericState); + transformedState = localReadTransformation.transformState(bundleContext, acceptedDataTypes, + numericState); } if (transformedState != null) { @@ -846,7 +868,7 @@ private Map processUpdatedValue(State numericState, boolean b "Channel {} will be updated to '{}' (type {}). Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}", channelId, transformedState, transformedState.getClass().getSimpleName(), numericState, readValueType, boolValue, - readTransformation.isIdentityTransform() ? "" : readTransformation); + localReadTransformation.isIdentityTransform() ? "" : localReadTransformation); states.put(channelUID, transformedState); } else { String types = StringUtils.join(acceptedDataTypes.stream().map(cls -> cls.getSimpleName()).toArray(), @@ -854,7 +876,7 @@ private Map processUpdatedValue(State numericState, boolean b logger.warn( "Channel {} will not be updated since transformation was unsuccessful. Channel is expecting the following data types [{}]. Input data: number value {} (value type '{}' taken into account) and bool value {}. Transformation: {}", channelId, types, numericState, readValueType, boolValue, - readTransformation.isIdentityTransform() ? "" : readTransformation); + localReadTransformation.isIdentityTransform() ? "" : localReadTransformation); } }); @@ -876,6 +898,8 @@ private void updateExpiredChannels(Map states) { } } + // since lastState can be null, and "lastState == null" in conditional is not useless + @SuppressWarnings("null") private void updateExpiredChannel(long now, ChannelUID uid, State state) { @Nullable State lastState = channelLastState.get(uid); diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusPollerThingHandler.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusPollerThingHandler.java deleted file mode 100644 index 1f765157be84a..0000000000000 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusPollerThingHandler.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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.internal.handler; - -import java.util.function.Supplier; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.io.transport.modbus.ModbusManager; -import org.openhab.io.transport.modbus.PollTask; - -/** - * Interface for poller thing handlers - * - * @author Sami Salonen - Initial contribution - * - */ -@NonNullByDefault -public interface ModbusPollerThingHandler { - - /** - * Return {@link PollTask} represented by this thing. - * - * Note that the poll task might be null in case initialization is not complete. - * - * @return poll task represented by this poller - */ - public @Nullable PollTask getPollTask(); - - /** - * Get {@link ModbusManager} supplier - * - * @return supplier of ModbusManger - */ - public Supplier getManagerRef(); - - /** - * Refresh data - */ - public void refresh(); -} diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusSerialThingHandler.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusSerialThingHandler.java index 6eb3a4cc949c9..1b2d7dfbd86f3 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusSerialThingHandler.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusSerialThingHandler.java @@ -15,10 +15,8 @@ import java.util.Collection; import java.util.Collections; import java.util.Optional; -import java.util.function.Supplier; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; @@ -38,8 +36,8 @@ public class ModbusSerialThingHandler extends AbstractModbusEndpointThingHandler { - public ModbusSerialThingHandler(Bridge bridge, Supplier managerRef) { - super(bridge, managerRef); + public ModbusSerialThingHandler(Bridge bridge, ModbusManager manager) { + super(bridge, manager); } @Override @@ -86,11 +84,12 @@ public boolean isDiscoveryEnabled() { } } + @SuppressWarnings("null") // Since endpoint in Optional.map cannot be null @Override - protected String formatConflictingParameterError(@Nullable EndpointPoolConfiguration otherPoolConfig) { + protected String formatConflictingParameterError() { return String.format( - "Endpoint '%s' has conflicting parameters: parameters of this thing (%s '%s') %s are different from some other things parameter: %s. Ensure that all endpoints pointing to serial port '%s' have same parameters.", - endpoint, thing.getUID(), this.thing.getLabel(), this.poolConfiguration, otherPoolConfig, + "Endpoint '%s' has conflicting parameters: parameters of this thing (%s '%s') are different from some other thing's parameter. Ensure that all endpoints pointing to serial port '%s' have same parameters.", + endpoint, thing.getUID(), this.thing.getLabel(), Optional.ofNullable(this.endpoint).map(e -> e.getPortName()).orElse("")); } diff --git a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusTcpThingHandler.java b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusTcpThingHandler.java index e7432e3fe8590..9f38887116e95 100644 --- a/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusTcpThingHandler.java +++ b/bundles/org.openhab.binding.modbus/src/main/java/org/openhab/binding/modbus/internal/handler/ModbusTcpThingHandler.java @@ -15,10 +15,8 @@ import java.util.Collection; import java.util.Collections; import java.util.Optional; -import java.util.function.Supplier; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; @@ -38,8 +36,8 @@ public class ModbusTcpThingHandler extends AbstractModbusEndpointThingHandler { - public ModbusTcpThingHandler(Bridge bridge, Supplier managerRef) { - super(bridge, managerRef); + public ModbusTcpThingHandler(Bridge bridge, ModbusManager manager) { + super(bridge, manager); } @Override @@ -63,21 +61,23 @@ protected void configure() throws ModbusConfigurationException { poolConfiguration.setReconnectAfterMillis(config.getReconnectAfterMillis()); } + @SuppressWarnings("null") // since Optional.map is always called with NonNull argument @Override - protected String formatConflictingParameterError(@Nullable EndpointPoolConfiguration otherPoolConfig) { + protected String formatConflictingParameterError() { return String.format( - "Endpoint '%s' has conflicting parameters: parameters of this thing (%s '%s') %s are different from some other things parameter: %s. Ensure that all endpoints pointing to tcp slave '%s:%s' have same parameters.", - endpoint, thing.getUID(), this.thing.getLabel(), this.poolConfiguration, otherPoolConfig, + "Endpoint '%s' has conflicting parameters: parameters of this thing (%s '%s') are different from some other thing's parameter. Ensure that all endpoints pointing to tcp slave '%s:%s' have same parameters.", + endpoint, thing.getUID(), this.thing.getLabel(), Optional.ofNullable(this.endpoint).map(e -> e.getAddress()).orElse(""), Optional.ofNullable(this.endpoint).map(e -> String.valueOf(e.getPort())).orElse("")); } @Override public int getSlaveId() { - if (config == null) { + ModbusTcpConfiguration localConfig = config; + if (localConfig == null) { throw new IllegalStateException("Poller not configured, but slave id is queried!"); } - return config.getId(); + return localConfig.getId(); } @Override diff --git a/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/AtomicStampedKeyValueTest.java b/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/AtomicStampedKeyValueTest.java index ec861bcbd5aee..ba852f6e00c49 100644 --- a/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/AtomicStampedKeyValueTest.java +++ b/bundles/org.openhab.binding.modbus/src/test/java/org/openhab/binding/modbus/internal/AtomicStampedKeyValueTest.java @@ -25,98 +25,59 @@ @RunWith(MockitoJUnitRunner.class) public class AtomicStampedKeyValueTest { - @Test(expected = NullPointerException.class) - public void testInitWithNullKey() { - new AtomicStampedKeyValue<>(0, null, new Object()); - } - @Test(expected = NullPointerException.class) public void testInitWithNullValue() { - new AtomicStampedKeyValue<>(0, new Object(), null); + new AtomicStampedValue<>(0, null); } @Test public void testGetters() { - Object key = new Object(); Object val = new Object(); - AtomicStampedKeyValue keyValue = new AtomicStampedKeyValue<>(42L, key, val); + AtomicStampedValue keyValue = new AtomicStampedValue<>(42L, val); assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(equalTo(key))); assertThat(keyValue.getValue(), is(equalTo(val))); } - @Test - public void testUpdateWithSameStampAndKey() { - Object key = new Object(); - Object val = new Object(); - AtomicStampedKeyValue keyValue = new AtomicStampedKeyValue<>(42L, key, val); - keyValue.update(42L, key, new Object()); - assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(equalTo(key))); - assertThat(keyValue.getValue(), is(not(equalTo(val)))); - } - @Test public void testUpdateWithSameStamp() { - Object key = new Object(); Object val = new Object(); - AtomicStampedKeyValue keyValue = new AtomicStampedKeyValue<>(42L, key, val); - keyValue.update(42L, new Object(), new Object()); + AtomicStampedValue keyValue = new AtomicStampedValue<>(42L, val); + keyValue.update(42L, new Object()); assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(not(equalTo(key)))); assertThat(keyValue.getValue(), is(not(equalTo(val)))); } @Test - public void testUpdateWithSameKey() { - Object key = new Object(); + public void testUpdateWithDifferentStamp() { Object val = new Object(); - AtomicStampedKeyValue keyValue = new AtomicStampedKeyValue<>(42L, key, val); - keyValue.update(-99L, key, new Object()); + AtomicStampedValue keyValue = new AtomicStampedValue<>(42L, val); + keyValue.update(-99L, new Object()); assertThat(keyValue.getStamp(), is(equalTo(-99L))); - assertThat(keyValue.getKey(), is(equalTo(key))); assertThat(keyValue.getValue(), is(not(equalTo(val)))); } - @Test - public void testUpdateWithSameValue() { - Object key = new Object(); - Object val = new Object(); - AtomicStampedKeyValue keyValue = new AtomicStampedKeyValue<>(42L, key, val); - keyValue.update(-99L, new Object(), val); - assertThat(keyValue.getStamp(), is(equalTo(-99L))); - assertThat(keyValue.getKey(), is(not(equalTo(key)))); - assertThat(keyValue.getValue(), is(equalTo(val))); - } - @Test public void testCopy() { - Object key = new Object(); Object val = new Object(); - AtomicStampedKeyValue keyValue = new AtomicStampedKeyValue<>(42L, key, val); - AtomicStampedKeyValue copy = keyValue.copy(); + AtomicStampedValue keyValue = new AtomicStampedValue<>(42L, val); + AtomicStampedValue copy = keyValue.copy(); - // keyValue unchanged + // unchanged assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(equalTo(key))); assertThat(keyValue.getValue(), is(equalTo(val))); // data matches assertThat(keyValue.getStamp(), is(equalTo(copy.getStamp()))); - assertThat(keyValue.getKey(), is(equalTo(copy.getKey()))); assertThat(keyValue.getValue(), is(equalTo(copy.getValue()))); // after update they live life of their own - Object key2 = new Object(); Object val2 = new Object(); - copy.update(-99L, key2, val2); + copy.update(-99L, val2); assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(equalTo(key))); assertThat(keyValue.getValue(), is(equalTo(val))); assertThat(copy.getStamp(), is(equalTo(-99L))); - assertThat(copy.getKey(), is(equalTo(key2))); assertThat(copy.getValue(), is(equalTo(val2))); } @@ -127,30 +88,26 @@ public void testCopy() { public void testCopyIfStampAfterEqual() { Object key = new Object(); Object val = new Object(); - AtomicStampedKeyValue keyValue = new AtomicStampedKeyValue<>(42L, key, val); - AtomicStampedKeyValue copy = keyValue.copyIfStampAfter(42L); + AtomicStampedValue keyValue = new AtomicStampedValue<>(42L, val); + AtomicStampedValue copy = keyValue.copyIfStampAfter(42L); // keyValue unchanged assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(equalTo(key))); assertThat(keyValue.getValue(), is(equalTo(val))); // data matches assertThat(keyValue.getStamp(), is(equalTo(copy.getStamp()))); - assertThat(keyValue.getKey(), is(equalTo(copy.getKey()))); assertThat(keyValue.getValue(), is(equalTo(copy.getValue()))); // after update they live life of their own Object key2 = new Object(); Object val2 = new Object(); - copy.update(-99L, key2, val2); + copy.update(-99L, val2); assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(equalTo(key))); assertThat(keyValue.getValue(), is(equalTo(val))); assertThat(copy.getStamp(), is(equalTo(-99L))); - assertThat(copy.getKey(), is(equalTo(key2))); assertThat(copy.getValue(), is(equalTo(val2))); } @@ -159,14 +116,12 @@ public void testCopyIfStampAfterEqual() { */ @Test public void testCopyIfStampAfterTooOld() { - Object key = new Object(); Object val = new Object(); - AtomicStampedKeyValue keyValue = new AtomicStampedKeyValue<>(42L, key, val); - AtomicStampedKeyValue copy = keyValue.copyIfStampAfter(43L); + AtomicStampedValue keyValue = new AtomicStampedValue<>(42L, val); + AtomicStampedValue copy = keyValue.copyIfStampAfter(43L); // keyValue unchanged assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(equalTo(key))); assertThat(keyValue.getValue(), is(equalTo(val))); // copy is null @@ -178,47 +133,42 @@ public void testCopyIfStampAfterTooOld() { */ @Test public void testCopyIfStampAfterFresh() { - Object key = new Object(); Object val = new Object(); - AtomicStampedKeyValue keyValue = new AtomicStampedKeyValue<>(42L, key, val); - AtomicStampedKeyValue copy = keyValue.copyIfStampAfter(41L); + AtomicStampedValue keyValue = new AtomicStampedValue<>(42L, val); + AtomicStampedValue copy = keyValue.copyIfStampAfter(41L); // keyValue unchanged assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(equalTo(key))); assertThat(keyValue.getValue(), is(equalTo(val))); // data matches assertThat(keyValue.getStamp(), is(equalTo(copy.getStamp()))); - assertThat(keyValue.getKey(), is(equalTo(copy.getKey()))); assertThat(keyValue.getValue(), is(equalTo(copy.getValue()))); // after update they live life of their own Object key2 = new Object(); Object val2 = new Object(); - copy.update(-99L, key2, val2); + copy.update(-99L, val2); assertThat(keyValue.getStamp(), is(equalTo(42L))); - assertThat(keyValue.getKey(), is(equalTo(key))); assertThat(keyValue.getValue(), is(equalTo(val))); assertThat(copy.getStamp(), is(equalTo(-99L))); - assertThat(copy.getKey(), is(equalTo(key2))); assertThat(copy.getValue(), is(equalTo(val2))); } @Test public void testCompare() { // equal, smaller, larger - assertThat(AtomicStampedKeyValue.compare(new AtomicStampedKeyValue<>(42L, "", ""), - new AtomicStampedKeyValue<>(42L, "", "")), is(equalTo(0))); - assertThat(AtomicStampedKeyValue.compare(new AtomicStampedKeyValue<>(41L, "", ""), - new AtomicStampedKeyValue<>(42L, "", "")), is(equalTo(-1))); - assertThat(AtomicStampedKeyValue.compare(new AtomicStampedKeyValue<>(42L, "", ""), - new AtomicStampedKeyValue<>(41L, "", "")), is(equalTo(1))); + assertThat(AtomicStampedValue.compare(new AtomicStampedValue<>(42L, ""), new AtomicStampedValue<>(42L, "")), + is(equalTo(0))); + assertThat(AtomicStampedValue.compare(new AtomicStampedValue<>(41L, ""), new AtomicStampedValue<>(42L, "")), + is(equalTo(-1))); + assertThat(AtomicStampedValue.compare(new AtomicStampedValue<>(42L, ""), new AtomicStampedValue<>(41L, "")), + is(equalTo(1))); // Nulls come first - assertThat(AtomicStampedKeyValue.compare(null, new AtomicStampedKeyValue<>(42L, "", "")), is(equalTo(-1))); - assertThat(AtomicStampedKeyValue.compare(new AtomicStampedKeyValue<>(42L, "", ""), null), is(equalTo(1))); + assertThat(AtomicStampedValue.compare(null, new AtomicStampedValue<>(42L, "")), is(equalTo(-1))); + assertThat(AtomicStampedValue.compare(new AtomicStampedValue<>(42L, ""), null), is(equalTo(1))); } } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusFailure.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusFailure.java new file mode 100644 index 0000000000000..09917e87b25ec --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusFailure.java @@ -0,0 +1,65 @@ +/** + * 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.io.transport.modbus; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Encapsulates result of modbus read operations + * + * @author Nagy Attila Gabor - Initial contribution + */ +@NonNullByDefault +public class AsyncModbusFailure { + private final R request; + + private final Exception cause; + + public AsyncModbusFailure(R request, Exception cause) { + Objects.requireNonNull(request, "Request must not be null!"); + Objects.requireNonNull(cause, "Cause must not be null!"); + this.request = request; + this.cause = cause; + } + + /** + * Get request matching this response + * + * @return request object + */ + public R getRequest() { + return request; + } + + /** + * Get cause of error + * + * @return exception representing error + */ + public Exception getCause() { + return cause; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("AsyncModbusReadResult("); + builder.append("request = "); + builder.append(request); + builder.append(", error = "); + builder.append(cause); + builder.append(")"); + return builder.toString(); + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusReadResult.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusReadResult.java new file mode 100644 index 0000000000000..0d02b11de6798 --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusReadResult.java @@ -0,0 +1,93 @@ +/** + * 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.io.transport.modbus; + +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Encapsulates result of modbus read operations + * + * @author Sami Salonen - Initial contribution + */ +@NonNullByDefault +public class AsyncModbusReadResult { + + private final ModbusReadRequestBlueprint request; + + private final Optional bits; + + private final Optional registers; + + public AsyncModbusReadResult(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { + Objects.requireNonNull(request, "Request must not be null!"); + Objects.requireNonNull(registers, "Registers must not be null!"); + this.request = request; + this.registers = Optional.of(registers); + this.bits = Optional.empty(); + } + + public AsyncModbusReadResult(ModbusReadRequestBlueprint request, BitArray bits) { + Objects.requireNonNull(request, "Request must not be null!"); + Objects.requireNonNull(bits, "Bits must not be null!"); + this.request = request; + this.registers = Optional.empty(); + this.bits = Optional.of(bits); + } + + /** + * Get request matching this response + * + * @return request object + */ + public ModbusReadRequestBlueprint getRequest() { + return request; + } + + /** + * Get "coil" or "discrete input" bit data in the case of no errors + * + * @return bit data + */ + public Optional getBits() { + return bits; + } + + /** + * Get "input register" or "holding register" data in the case of no errors + * + * @return register data + */ + public Optional getRegisters() { + return registers; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("AsyncModbusReadResult("); + builder.append("request = "); + builder.append(request); + bits.ifPresent(bits -> { + builder.append(", bits = "); + builder.append(bits); + }); + registers.ifPresent(registers -> { + builder.append(", registers = "); + builder.append(registers); + }); + builder.append(")"); + return builder.toString(); + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusWriteResult.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusWriteResult.java new file mode 100644 index 0000000000000..b5b3a4eda55ff --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/AsyncModbusWriteResult.java @@ -0,0 +1,66 @@ +/** + * 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.io.transport.modbus; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Encapsulates result of modbus write operations + * + * @author Sami Salonen - Initial contribution + */ +@NonNullByDefault +public class AsyncModbusWriteResult { + + private final ModbusWriteRequestBlueprint request; + + private final ModbusResponse response; + + public AsyncModbusWriteResult(ModbusWriteRequestBlueprint request, ModbusResponse response) { + Objects.requireNonNull(request, "Request must not be null!"); + Objects.requireNonNull(response, "Response must not be null!"); + this.request = request; + this.response = response; + } + + /** + * Get request matching this response + * + * @return request object + */ + public ModbusWriteRequestBlueprint getRequest() { + return request; + } + + /** + * Get response + * + * @return response + */ + public ModbusResponse getResponse() { + return response; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("AsyncModbusWriteResult("); + builder.append("request = "); + builder.append(request); + builder.append(", response = "); + builder.append(response); + builder.append(")"); + return builder.toString(); + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicBitArray.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicBitArray.java deleted file mode 100644 index dbf8bc17f8041..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicBitArray.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 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.io.transport.modbus; - -import java.util.BitSet; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * Class that implements a collection for - * bits - * - * @author Sami Salonen - Initial contribution - */ -@NonNullByDefault -public class BasicBitArray implements BitArray { - - private BitSet wrapped; - private int length; - - public BasicBitArray(int nbits) { - this(new BitSet(nbits), nbits); - } - - public BasicBitArray(boolean... bits) { - this(bitSetFromBooleans(bits), bits.length); - } - - public BasicBitArray(BitSet wrapped, int length) { - this.wrapped = wrapped; - this.length = length; - } - - private static BitSet bitSetFromBooleans(boolean... bits) { - BitSet bitSet = new BitSet(bits.length); - for (int i = 0; i < bits.length; i++) { - bitSet.set(i, bits[i]); - } - - return bitSet; - } - - @Override - public boolean getBit(int index) { - if (index >= size()) { - throw new IndexOutOfBoundsException(); - } - return this.wrapped.get(index); - } - - public void setBit(int index, boolean value) { - if (value) { - this.wrapped.set(index); - } else { - this.wrapped.clear(index); - } - } - - @Override - public int size() { - return length; - } - - @Override - public String toString() { - return new StringBuilder("BitArrayImpl(bits=").append(length == 0 ? "" : toBinaryString()).append(")") - .toString(); - } - - @Override - public boolean equals(@Nullable Object obj) { - return sizeAndValuesEquals(obj); - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusReadRequestBlueprint.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusReadRequestBlueprint.java deleted file mode 100644 index f3ad6c59adbf9..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusReadRequestBlueprint.java +++ /dev/null @@ -1,107 +0,0 @@ -/** - * 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.io.transport.modbus; - -import org.apache.commons.lang.builder.EqualsBuilder; -import org.apache.commons.lang.builder.HashCodeBuilder; -import org.apache.commons.lang.builder.StandardToStringStyle; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * Immutable implementation of {@link ModbusReadRequestBlueprint} - * - * Equals and hashCode implemented keeping {@link PollTask} in mind: two instances of this class are considered the same - * if they have - * the equal parameters (same slave id, start, length, function code and maxTries). - * - * @author Sami Salonen - Initial contribution - * - */ -@NonNullByDefault -public class BasicModbusReadRequestBlueprint implements ModbusReadRequestBlueprint { - private static StandardToStringStyle toStringStyle = new StandardToStringStyle(); - static { - toStringStyle.setUseShortClassName(true); - } - - private int slaveId; - private ModbusReadFunctionCode functionCode; - private int start; - private int length; - private int maxTries; - - public BasicModbusReadRequestBlueprint(int slaveId, ModbusReadFunctionCode functionCode, int start, int length, - int maxTries) { - super(); - this.slaveId = slaveId; - this.functionCode = functionCode; - this.start = start; - this.length = length; - this.maxTries = maxTries; - } - - @Override - public int getUnitID() { - return slaveId; - } - - @Override - public int getReference() { - return start; - } - - @Override - public ModbusReadFunctionCode getFunctionCode() { - return functionCode; - } - - @Override - public int getDataLength() { - return length; - } - - @Override - public int getMaxTries() { - return maxTries; - } - - @Override - public int hashCode() { - return new HashCodeBuilder(81, 3).append(slaveId).append(functionCode).append(start).append(length) - .append(maxTries).toHashCode(); - } - - @Override - public String toString() { - return new ToStringBuilder(this, toStringStyle).append("slaveId", slaveId).append("functionCode", functionCode) - .append("start", start).append("length", length).append("maxTries", maxTries).toString(); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj == null) { - return false; - } - if (obj == this) { - return true; - } - if (obj.getClass() != getClass()) { - return false; - } - BasicModbusReadRequestBlueprint rhs = (BasicModbusReadRequestBlueprint) obj; - return new EqualsBuilder().append(slaveId, rhs.slaveId).append(functionCode, rhs.functionCode) - .append(start, rhs.start).append(length, rhs.length).isEquals(); - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusRegister.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusRegister.java deleted file mode 100644 index c1f8d4d88ca88..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusRegister.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * 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.io.transport.modbus; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -import net.wimpi.modbus.procimg.SimpleInputRegister; - -/** - * Basic {@link ModbusRegister} implementation - * - * @author Sami Salonen - Initial contribution - */ -@NonNullByDefault -public class BasicModbusRegister implements ModbusRegister { - - private SimpleInputRegister wrapped; - - /** - * Constructs a new instance for bytes - * - * @param b1 the first (hi) byte of the word. - * @param b2 the second (low) byte of the word. - */ - public BasicModbusRegister(byte b1, byte b2) { - wrapped = new SimpleInputRegister(b1, b2); - } - - /** - * Construct register for at - * - * @param val value representing register data. The int will be downcasted to short. - */ - public BasicModbusRegister(int val) { - wrapped = new SimpleInputRegister(val); - } - - @Override - public byte[] getBytes() { - return wrapped.toBytes(); - } - - @Override - public int getValue() { - return wrapped.getValue(); - } - - @Override - public int toUnsignedShort() { - return wrapped.toUnsignedShort(); - } - - @Override - public String toString() { - StringBuffer buffer = new StringBuffer("ModbusRegisterImpl("); - buffer.append("uint16=").append(toUnsignedShort()).append(", hex="); - return appendHexString(buffer).append(')').toString(); - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusRegisterArray.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusRegisterArray.java deleted file mode 100644 index 90f08a525922a..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusRegisterArray.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * 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.io.transport.modbus; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * Immutable {@link ModbusRegisterArray} implementation - * - * @author Sami Salonen - Initial contribution - */ -@NonNullByDefault -public class BasicModbusRegisterArray implements ModbusRegisterArray { - - private ModbusRegister[] registers; - - /** - * Construct plain ModbusRegister[] array from register values - * - * @param registerValues register values, each int corresponding to one register - * @return - */ - public static ModbusRegister[] registersFromValues(int... registerValues) { - ModbusRegister[] registers = new ModbusRegister[registerValues.length]; - for (int i = 0; i < registerValues.length; i++) { - registers[i] = new BasicModbusRegister(registerValues[i]); - } - return registers; - } - - /** - * Construct ModbusRegisterArrayImpl from array of {@link ModbusRegister} - * - * @param registers - */ - public BasicModbusRegisterArray(ModbusRegister[] registers) { - this.registers = registers; - } - - /** - * Construct plain ModbusRegisterArrayImpl array from register values - * - * @param registerValues register values, each int corresponding to one register - * @return - */ - public BasicModbusRegisterArray(int... registerValues) { - this(registersFromValues(registerValues)); - } - - @Override - public ModbusRegister getRegister(int index) { - return registers[index]; - } - - @Override - public int size() { - return registers.length; - } - - @Override - public String toString() { - if (registers.length == 0) { - return "ModbusRegisterArrayImpl()"; - } - StringBuffer buffer = new StringBuffer(registers.length * 2).append("ModbusRegisterArrayImpl("); - return appendHexString(buffer).append(')').toString(); - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusWriteCoilRequestBlueprint.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusWriteCoilRequestBlueprint.java deleted file mode 100644 index 0fa478c531c35..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusWriteCoilRequestBlueprint.java +++ /dev/null @@ -1,135 +0,0 @@ -/** - * 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.io.transport.modbus; - -import org.apache.commons.lang.builder.StandardToStringStyle; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * Implementation for writing coils - * - * @author Sami Salonen - Initial contribution - * - */ -@NonNullByDefault -public class BasicModbusWriteCoilRequestBlueprint implements ModbusWriteCoilRequestBlueprint { - - private static StandardToStringStyle toStringStyle = new StandardToStringStyle(); - - static { - toStringStyle.setUseShortClassName(true); - } - - /** - * Implementation of {@link BitArray} with single bit as data - * - * @author Sami Salonen - Initial contribution - * - */ - private static class SingleBitArray extends BasicBitArray { - - public SingleBitArray(boolean bit) { - super(bit); - } - - @Override - public String toString() { - return "SingleBitArray(bit=" + toBinaryString() + ")"; - } - } - - private int slaveId; - private int reference; - private BitArray bits; - private boolean writeMultiple; - private int maxTries; - - /** - * Construct coil write request with single bit of data - * - * @param slaveId slave id to write to - * @param reference reference address - * @param data bit to write - * @param writeMultiple whether to use {@link ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS} over - * {@link ModbusWriteFunctionCode.WRITE_COIL} - * @param maxTries maximum number of tries in case of errors, should be at least 1 - */ - public BasicModbusWriteCoilRequestBlueprint(int slaveId, int reference, boolean data, boolean writeMultiple, - int maxTries) { - this(slaveId, reference, new SingleBitArray(data), writeMultiple, maxTries); - } - - /** - * Construct coil write request with many bits of data - * - * @param slaveId slave id to write to - * @param reference reference address - * @param data bit(s) to write - * @param writeMultiple whether to use {@link ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS} over - * {@link ModbusWriteFunctionCode.WRITE_COIL}. Useful with single bit of data. - * @param maxTries maximum number of tries in case of errors, should be at least 1 - * @throws IllegalArgumentException in case data is empty, writeMultiple is - * false but there are many bits to write. - */ - public BasicModbusWriteCoilRequestBlueprint(int slaveId, int reference, BitArray data, boolean writeMultiple, - int maxTries) { - super(); - this.slaveId = slaveId; - this.reference = reference; - this.bits = data; - this.writeMultiple = writeMultiple; - this.maxTries = maxTries; - - if (!writeMultiple && bits.size() > 1) { - throw new IllegalArgumentException("With multiple coils, writeMultiple must be true"); - } - if (bits.size() == 0) { - throw new IllegalArgumentException("Must have at least one bit"); - } - if (maxTries <= 0) { - throw new IllegalArgumentException("maxTries should be positive, was " + maxTries); - } - } - - @Override - public int getUnitID() { - return slaveId; - } - - @Override - public int getReference() { - return reference; - } - - @Override - public ModbusWriteFunctionCode getFunctionCode() { - return writeMultiple ? ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS : ModbusWriteFunctionCode.WRITE_COIL; - } - - @Override - public BitArray getCoils() { - return bits; - } - - @Override - public int getMaxTries() { - return maxTries; - } - - @Override - public String toString() { - return new ToStringBuilder(this, toStringStyle).append("slaveId", slaveId).append("reference", reference) - .append("functionCode", getFunctionCode()).append("bits", bits).append("maxTries", maxTries).toString(); - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusWriteRegisterRequestBlueprint.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusWriteRegisterRequestBlueprint.java deleted file mode 100644 index e84f1426a362e..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicModbusWriteRegisterRequestBlueprint.java +++ /dev/null @@ -1,104 +0,0 @@ -/** - * 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.io.transport.modbus; - -import org.apache.commons.lang.builder.StandardToStringStyle; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * Implementation for writing registers - * - * @author Sami Salonen - Initial contribution - * - */ -@NonNullByDefault -public class BasicModbusWriteRegisterRequestBlueprint implements ModbusWriteRegisterRequestBlueprint { - - private static StandardToStringStyle toStringStyle = new StandardToStringStyle(); - - static { - toStringStyle.setUseShortClassName(true); - } - - private int slaveId; - private int reference; - private ModbusRegisterArray registers; - private boolean writeMultiple; - private int maxTries; - - /** - * Construct coil write request with many bits of data - * - * @param slaveId slave id to write to - * @param reference reference address - * @param registers register(s) to write - * @param writeMultiple whether to use {@link ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS} over - * {@link ModbusWriteFunctionCode.WRITE_COIL}. Useful with single register of data. - * @param maxTries maximum number of tries in case of errors, should be at least 1 - * @throws IllegalArgumentException in case data is empty, writeMultiple is - * false but there are many registers to write. - */ - public BasicModbusWriteRegisterRequestBlueprint(int slaveId, int reference, ModbusRegisterArray registers, - boolean writeMultiple, int maxTries) throws IllegalArgumentException { - super(); - this.slaveId = slaveId; - this.reference = reference; - this.registers = registers; - this.writeMultiple = writeMultiple; - this.maxTries = maxTries; - - if (!writeMultiple && registers.size() > 1) { - throw new IllegalArgumentException("With multiple registers, writeMultiple must be true"); - } - if (registers.size() == 0) { - throw new IllegalArgumentException("Must have at least one register"); - } - if (maxTries <= 0) { - throw new IllegalArgumentException("maxTries should be positive"); - } - } - - @Override - public int getReference() { - return reference; - } - - @Override - public int getUnitID() { - return slaveId; - } - - @Override - public ModbusWriteFunctionCode getFunctionCode() { - return writeMultiple ? ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS - : ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER; - } - - @Override - public ModbusRegisterArray getRegisters() { - return registers; - } - - @Override - public int getMaxTries() { - return maxTries; - } - - @Override - public String toString() { - return new ToStringBuilder(this, toStringStyle).append("slaveId", slaveId).append("reference", reference) - .append("functionCode", getFunctionCode()).append("registers", registers).append("maxTries", maxTries) - .toString(); - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BitArray.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BitArray.java index 5b969b03be5f7..6b73eceb390f8 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BitArray.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BitArray.java @@ -12,6 +12,7 @@ */ package org.openhab.io.transport.modbus; +import java.util.BitSet; import java.util.Iterator; import java.util.stream.IntStream; @@ -25,33 +26,34 @@ * @author Sami Salonen - Initial contribution */ @NonNullByDefault -public interface BitArray extends Iterable { - /** - * Returns the state of the bit at the given index - * - * Index 0 matches LSB (rightmost) bit - *

- * - * @param index the index of the bit to be returned. - * @return true if the bit at the specified index is set, - * false otherwise. - * @throws IndexOutOfBoundsException if the index is out of bounds. - */ - public boolean getBit(int index); +public class BitArray implements Iterable { - /** - * Get number of bits stored in this instance - * - * @return - */ - public int size(); + private final BitSet wrapped; + private final int length; - @Override - public default Iterator iterator() { - return IntStream.range(0, size()).mapToObj(i -> getBit(i)).iterator(); + public BitArray(int nbits) { + this(new BitSet(nbits), nbits); + } + + public BitArray(boolean... bits) { + this(bitSetFromBooleans(bits), bits.length); + } + + public BitArray(BitSet wrapped, int length) { + this.wrapped = wrapped; + this.length = length; } - public default boolean sizeAndValuesEquals(@Nullable Object obj) { + private static BitSet bitSetFromBooleans(boolean... bits) { + BitSet bitSet = new BitSet(bits.length); + for (int i = 0; i < bits.length; i++) { + bitSet.set(i, bits[i]); + } + + return bitSet; + } + + private boolean sizeAndValuesEquals(@Nullable Object obj) { if (obj == null) { return false; } @@ -73,6 +75,57 @@ public default boolean sizeAndValuesEquals(@Nullable Object obj) { return true; } + /** + * Returns the state of the bit at the given index + * + * Index 0 matches LSB (rightmost) bit + *

+ * + * @param index the index of the bit to be returned. + * @return true if the bit at the specified index is set, + * false otherwise. + * @throws IndexOutOfBoundsException if the index is out of bounds. + */ + public boolean getBit(int index) { + if (index >= size()) { + throw new IndexOutOfBoundsException(); + } + return this.wrapped.get(index); + } + + public void setBit(int index, boolean value) { + if (value) { + this.wrapped.set(index); + } else { + this.wrapped.clear(index); + } + } + + /** + * Get number of bits stored in this instance + * + * @return + */ + public int size() { + return length; + } + + @Override + public String toString() { + return new StringBuilder("BitArray(bits=").append(length == 0 ? "" : toBinaryString()).append(")") + .toString(); + } + + @Override + public Iterator iterator() { + return IntStream.range(0, size()).mapToObj(i -> getBit(i)).iterator(); + } + + @Override + public boolean equals(@Nullable Object obj) { + return sizeAndValuesEquals(obj); + } + /** * Get data as binary string * @@ -80,7 +133,7 @@ public default boolean sizeAndValuesEquals(@Nullable Object obj) { * * @return string representing the data */ - default String toBinaryString() { + public String toBinaryString() { final StringBuilder buffer = new StringBuilder(size()); IntStream.range(0, size()).mapToObj(i -> getBit(i) ? '1' : '0').forEach(buffer::append); return buffer.toString(); diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusBitUtilities.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusBitUtilities.java index e5834c68077c3..62a8839e96d13 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusBitUtilities.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusBitUtilities.java @@ -295,8 +295,8 @@ public static ModbusRegisterArray commandToRegisters(Command command, ModbusCons byte b1 = (byte) (shortValue >> 8); byte b2 = (byte) shortValue; - ModbusRegister register = new BasicModbusRegister(b1, b2); - return new BasicModbusRegisterArray(new ModbusRegister[] { register }); + ModbusRegister register = new ModbusRegister(b1, b2); + return new ModbusRegisterArray(new ModbusRegister[] { register }); } case INT32: case UINT32: { @@ -306,9 +306,9 @@ public static ModbusRegisterArray commandToRegisters(Command command, ModbusCons byte b2 = (byte) (intValue >> 16); byte b3 = (byte) (intValue >> 8); byte b4 = (byte) intValue; - ModbusRegister register = new BasicModbusRegister(b1, b2); - ModbusRegister register2 = new BasicModbusRegister(b3, b4); - return new BasicModbusRegisterArray(new ModbusRegister[] { register, register2 }); + ModbusRegister register = new ModbusRegister(b1, b2); + ModbusRegister register2 = new ModbusRegister(b3, b4); + return new ModbusRegisterArray(new ModbusRegister[] { register, register2 }); } case INT32_SWAP: case UINT32_SWAP: { @@ -318,9 +318,9 @@ public static ModbusRegisterArray commandToRegisters(Command command, ModbusCons byte b2 = (byte) (intValue >> 16); byte b3 = (byte) (intValue >> 8); byte b4 = (byte) intValue; - ModbusRegister register = new BasicModbusRegister(b3, b4); - ModbusRegister register2 = new BasicModbusRegister(b1, b2); - return new BasicModbusRegisterArray(new ModbusRegister[] { register, register2 }); + ModbusRegister register = new ModbusRegister(b3, b4); + ModbusRegister register2 = new ModbusRegister(b1, b2); + return new ModbusRegisterArray(new ModbusRegister[] { register, register2 }); } case FLOAT32: { float floatValue = numericCommand.floatValue(); @@ -330,9 +330,9 @@ public static ModbusRegisterArray commandToRegisters(Command command, ModbusCons byte b2 = (byte) (intBits >> 16); byte b3 = (byte) (intBits >> 8); byte b4 = (byte) intBits; - ModbusRegister register = new BasicModbusRegister(b1, b2); - ModbusRegister register2 = new BasicModbusRegister(b3, b4); - return new BasicModbusRegisterArray(new ModbusRegister[] { register, register2 }); + ModbusRegister register = new ModbusRegister(b1, b2); + ModbusRegister register2 = new ModbusRegister(b3, b4); + return new ModbusRegisterArray(new ModbusRegister[] { register, register2 }); } case FLOAT32_SWAP: { float floatValue = numericCommand.floatValue(); @@ -342,9 +342,9 @@ public static ModbusRegisterArray commandToRegisters(Command command, ModbusCons byte b2 = (byte) (intBits >> 16); byte b3 = (byte) (intBits >> 8); byte b4 = (byte) intBits; - ModbusRegister register = new BasicModbusRegister(b3, b4); - ModbusRegister register2 = new BasicModbusRegister(b1, b2); - return new BasicModbusRegisterArray(new ModbusRegister[] { register, register2 }); + ModbusRegister register = new ModbusRegister(b3, b4); + ModbusRegister register2 = new ModbusRegister(b1, b2); + return new ModbusRegisterArray(new ModbusRegister[] { register, register2 }); } case INT64: case UINT64: { @@ -358,9 +358,8 @@ public static ModbusRegisterArray commandToRegisters(Command command, ModbusCons byte b6 = (byte) (longValue >> 16); byte b7 = (byte) (longValue >> 8); byte b8 = (byte) longValue; - return new BasicModbusRegisterArray( - new ModbusRegister[] { new BasicModbusRegister(b1, b2), new BasicModbusRegister(b3, b4), - new BasicModbusRegister(b5, b6), new BasicModbusRegister(b7, b8) }); + return new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister(b1, b2), + new ModbusRegister(b3, b4), new ModbusRegister(b5, b6), new ModbusRegister(b7, b8) }); } case INT64_SWAP: case UINT64_SWAP: { @@ -374,9 +373,8 @@ public static ModbusRegisterArray commandToRegisters(Command command, ModbusCons byte b6 = (byte) (longValue >> 16); byte b7 = (byte) (longValue >> 8); byte b8 = (byte) longValue; - return new BasicModbusRegisterArray( - new ModbusRegister[] { new BasicModbusRegister(b7, b8), new BasicModbusRegister(b5, b6), - new BasicModbusRegister(b3, b4), new BasicModbusRegister(b1, b2) }); + return new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister(b7, b8), + new ModbusRegister(b5, b6), new ModbusRegister(b3, b4), new ModbusRegister(b1, b2) }); } default: throw new NotImplementedException( diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusCommunicationInterface.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusCommunicationInterface.java new file mode 100644 index 0000000000000..36f2eb21df791 --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusCommunicationInterface.java @@ -0,0 +1,96 @@ +/** + * 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.io.transport.modbus; + +import java.util.concurrent.Future; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; + +/** + * Interface for interacting with a particular modbus slave. + * + * When no further communication is expected with the slave, close the interface so that any underlying resources can be + * freed. + * + * Close unregisters all the regular polls registered with registerRegularPoll. When endpoint's last + * communication interface is closed, the connection is closed as well, no matter the what EndpointPoolConfiguration + * says. + * + * @author Sami Salonen - Initial contribution + * + */ +@NonNullByDefault +public interface ModbusCommunicationInterface extends AutoCloseable { + + /** + * Get endpoint associated with this communication interface + * + * @return modbus slave endpoint + */ + public ModbusSlaveEndpoint getEndpoint(); + + /** + * 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 failureCallback); + + /** + * 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 failureCallback); + + /** + * Unregister regularly polled task + * + * @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 + * @throws IllegalStateException when this communication has been closed already + */ + public boolean unregisterRegularPoll(PollTask task); + + /** + * 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 failureCallback); +} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusFailureCallback.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusFailureCallback.java new file mode 100644 index 0000000000000..c4215e284f911 --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusFailureCallback.java @@ -0,0 +1,31 @@ +/** + * 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.io.transport.modbus; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Callback used to report failure in Modbus + * + * @author Nagy Attila Gabor - Initial contribution + */ +@FunctionalInterface +@NonNullByDefault +public interface ModbusFailureCallback { + /** + * Callback handling response with error + * + * @param asyncModbusFailure details of the failure + */ + void handle(AsyncModbusFailure failure); +} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusManager.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusManager.java index cfd0d2f5574bb..163b88745a271 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusManager.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusManager.java @@ -12,9 +12,6 @@ */ package org.openhab.io.transport.modbus; -import java.util.Set; -import java.util.concurrent.ScheduledFuture; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration; @@ -29,49 +26,16 @@ public interface ModbusManager { /** - * Submit one-time poll task. The method returns immediately, and the execution of the poll task will happen in - * background. - * - * @param task - * @return future representing the polled task - */ - public ScheduledFuture submitOneTimePoll(PollTask task); - - /** - * Register regularly polled task. The method returns immediately, and the execution of the poll task will happen in - * the background. + * Open communication interface to endpoint * - * @param task - * @return + * @param endpoint endpoint pointing to modbus slave + * @param configuration configuration for the endpoint + * @return Communication interface for interacting with the slave + * @throws IllegalArgumentException if there is already open communication interface with same endpoint but + * differing configuration */ - public void registerRegularPoll(PollTask task, long pollPeriodMillis, long initialDelayMillis); - - /** - * Unregister regularly polled task - * - * @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); - - /** - * Submit one-time write task. The method returns immediately, and the execution of the task will happen in - * background. - * - * @param task - * @return future representing the task - */ - public ScheduledFuture submitOneTimeWrite(WriteTask task); - - /** - * Configure general connection settings with a given endpoint - * - * @param endpoint endpoint to configure - * @param configuration configuration for the endpoint. Use null to reset the configuration to default settings. - */ - public void setEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint, - @Nullable EndpointPoolConfiguration configuration); + public ModbusCommunicationInterface newModbusCommunicationInterface(ModbusSlaveEndpoint endpoint, + @Nullable EndpointPoolConfiguration configuration) throws IllegalArgumentException; /** * Get general configuration settings applied to a given endpoint @@ -82,25 +46,4 @@ public void setEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint, * @return general connection settings of the given endpoint */ public @Nullable EndpointPoolConfiguration getEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint); - - /** - * Register listener for changes - * - * @param listener - */ - public void addListener(ModbusManagerListener listener); - - /** - * Remove listener for changes - * - * @param listener - */ - public void removeListener(ModbusManagerListener listener); - - /** - * Get registered regular polls - * - * @return set of registered regular polls - */ - public Set getRegisteredRegularPolls(); } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusManagerListener.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusManagerListener.java deleted file mode 100644 index 8c800ca70bf54..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusManagerListener.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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.io.transport.modbus; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration; -import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; - -/** - * Interface for {@link ModbusManager} listeners - * - * @author Sami Salonen - Initial contribution - */ -@NonNullByDefault -public interface ModbusManagerListener { - - /** - * Called on every call for {@link ModbusManager.setEndpointPoolConfiguration} - * - * @param endpoint value passed in call of setEndpointPoolConfiguration - * @param configuration value passed in call of setEndpointPoolConfiguration - */ - public void onEndpointPoolConfigurationSet(ModbusSlaveEndpoint endpoint, - @Nullable EndpointPoolConfiguration configuration); -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusReadCallback.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusReadCallback.java index 44b1fb34fdce6..9c0492ff5479f 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusReadCallback.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusReadCallback.java @@ -19,31 +19,14 @@ * * @author Sami Salonen - Initial contribution */ +@FunctionalInterface @NonNullByDefault -public interface ModbusReadCallback extends ModbusCallback { +public interface ModbusReadCallback extends ModbusResultCallback { /** - * Callback for "input register" and "holding register" data in the case of no errors + * Callback handling response data * - * @param ModbusReadRequestBlueprint representing the request - * @param registers data received from slave device in the last pollInterval + * @param result result of the read operation */ - void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers); - - /** - * Callback for "coil" and "discrete input" bit data in the case of no errors - * - * @param ModbusReadRequestBlueprint representing the request - * @param bits data received from slave device - */ - void onBits(ModbusReadRequestBlueprint request, BitArray bits); - - /** - * Callback for errors with read - * - * @request ModbusRequestBlueprint representing the request - * @param Exception representing the issue with the request. Instance of - * {@link ModbusUnexpectedTransactionIdException} or {@link ModbusTransportException}. - */ - void onError(ModbusReadRequestBlueprint request, Exception error); + void handle(AsyncModbusReadResult result); } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusReadRequestBlueprint.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusReadRequestBlueprint.java index bf517e7da7e2d..67f81af517835 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusReadRequestBlueprint.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusReadRequestBlueprint.java @@ -12,47 +12,121 @@ */ package org.openhab.io.transport.modbus; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.StandardToStringStyle; +import org.apache.commons.lang.builder.ToStringBuilder; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import net.wimpi.modbus.Modbus; /** - * Low-level interface representing a read request + * Implementation of immutable representation of modbus read request + * + * Equals and hashCode implemented keeping {@link PollTask} in mind: two instances of this class are considered the same + * if they have + * the equal parameters (same slave id, start, length, function code and maxTries). * * @author Sami Salonen - Initial contribution * */ @NonNullByDefault -public interface ModbusReadRequestBlueprint extends ModbusRequestBlueprint { +public class ModbusReadRequestBlueprint { + private static StandardToStringStyle toStringStyle = new StandardToStringStyle(); + static { + toStringStyle.setUseShortClassName(true); + } + + private final int slaveId; + private final ModbusReadFunctionCode functionCode; + private final int start; + private final int length; + private final int maxTries; + + public ModbusReadRequestBlueprint(int slaveId, ModbusReadFunctionCode functionCode, int start, int length, + int maxTries) { + this.slaveId = slaveId; + this.functionCode = functionCode; + this.start = start; + this.length = length; + this.maxTries = maxTries; + } /** - * Returns the reference of the register/coil/discrete input to to start - * reading from with this - * ReadMultipleRegistersRequest. + * Returns the unit identifier of this + * ModbusMessage as int.
+ * The identifier is a 1-byte non negative + * integer value valid in the range of 0-255. *

* - * @return the reference of the register - * to start reading from as int. + * @return the unit identifier as int. */ - public int getReference(); + public int getUnitID() { + return slaveId; + } + + public int getReference() { + return start; + } + + public ModbusReadFunctionCode getFunctionCode() { + return functionCode; + } + + public int getDataLength() { + return length; + } /** - * Returns the number of registers/coils/discrete inputs + * Maximum number of tries to execute the request, when request fails + * + * For example, number 1 means on try only with no re-tries. * + * @return number of maximum tries */ - public int getDataLength(); + public int getMaxTries() { + return maxTries; + } /** - * Returns the function code of this + * Returns the protocol identifier of this * ModbusMessage as int.
- * The function code is a 1-byte non negative - * integer value valid in the range of 0-127.
- * Function codes are ordered in conformance - * classes their values are specified in - * net.wimpi.modbus.Modbus. + * The identifier is a 2-byte (short) non negative + * integer value valid in the range of 0-65535. *

* - * @return the function code as int. - * - * @see net.wimpi.modbus.Modbus + * @return the protocol identifier as int. */ - public ModbusReadFunctionCode getFunctionCode(); + public int getProtocolID() { + return Modbus.DEFAULT_PROTOCOL_ID; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(81, 3).append(slaveId).append(functionCode).append(start).append(length) + .append(maxTries).toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, toStringStyle).append("slaveId", slaveId).append("functionCode", functionCode) + .append("start", start).append("length", length).append("maxTries", maxTries).toString(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (obj.getClass() != getClass()) { + return false; + } + ModbusReadRequestBlueprint rhs = (ModbusReadRequestBlueprint) obj; + return new EqualsBuilder().append(slaveId, rhs.slaveId).append(functionCode, rhs.functionCode) + .append(start, rhs.start).append(length, rhs.length).isEquals(); + } } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegister.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegister.java index 22178b36700d5..7b787b02bb61e 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegister.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegister.java @@ -14,34 +14,70 @@ import org.eclipse.jdt.annotation.NonNullByDefault; +import net.wimpi.modbus.procimg.SimpleInputRegister; + /** - * Interface for 16 bit Modbus registers. + * Basic {@link ModbusRegister} implementation * * @author Sami Salonen - Initial contribution */ @NonNullByDefault -public interface ModbusRegister { +public class ModbusRegister { + + private final SimpleInputRegister wrapped; + + /** + * Constructs a new instance for bytes + * + * @param b1 the first (hi) byte of the word. + * @param b2 the second (low) byte of the word. + */ + public ModbusRegister(byte b1, byte b2) { + wrapped = new SimpleInputRegister(b1, b2); + } + + /** + * Construct register for at + * + * @param val value representing register data. The int will be downcasted to short. + */ + public ModbusRegister(int val) { + wrapped = new SimpleInputRegister(val); + } /** * Get raw data represented by this register. Since register is 16 bits, array of length 2 will be returned. * * @return byte array of length 2, high byte first. */ - public byte[] getBytes(); + public byte[] getBytes() { + return wrapped.toBytes(); + } /** * Returns the value of this register as integer representing 16 bit data parsed as signed integer. * * @return the register content as unsigned integer */ - public int getValue(); + public int getValue() { + return wrapped.getValue(); + } /** * Returns the value of this register as integer representing 16 bit data parsed as unsigned integer. * * @return the register content as unsigned integer */ - public int toUnsignedShort(); + public int toUnsignedShort() { + return wrapped.toUnsignedShort(); + } + + @Override + public String toString() { + StringBuffer buffer = new StringBuffer("ModbusRegisterImpl("); + buffer.append("uint16=").append(toUnsignedShort()).append(", hex="); + return appendHexString(buffer).append(')').toString(); + } /** * Returns the register value as hex string @@ -50,7 +86,7 @@ public interface ModbusRegister { * * @return string representing the register data */ - default String toHexString() { + public String toHexString() { StringBuffer buffer = new StringBuffer(5); return appendHexString(buffer).toString(); } @@ -59,7 +95,7 @@ default String toHexString() { * Appends the register value as hex string to the given StringBuffer * */ - default StringBuffer appendHexString(StringBuffer buffer) { + public StringBuffer appendHexString(StringBuffer buffer) { byte[] bytes = getBytes(); for (int i = 0; i < 2; i++) { byte b = bytes[i]; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegisterArray.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegisterArray.java index 42527279c3489..a6df40ea2772d 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegisterArray.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRegisterArray.java @@ -18,12 +18,48 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * Interface for immutable sequence of Modbus registers + * Immutable {@link ModbusRegisterArray} implementation * * @author Sami Salonen - Initial contribution */ @NonNullByDefault -public interface ModbusRegisterArray extends Iterable { +public class ModbusRegisterArray implements Iterable { + + private final ModbusRegister[] registers; + + /** + * Construct plain ModbusRegister[] array from register values + * + * @param registerValues register values, each int corresponding to one register + * @return + */ + public static ModbusRegister[] registersFromValues(int... registerValues) { + ModbusRegister[] registers = new ModbusRegister[registerValues.length]; + for (int i = 0; i < registerValues.length; i++) { + registers[i] = new ModbusRegister(registerValues[i]); + } + return registers; + } + + /** + * Construct ModbusRegisterArrayImpl from array of {@link ModbusRegister} + * + * @param registers + */ + public ModbusRegisterArray(ModbusRegister[] registers) { + this.registers = registers; + } + + /** + * Construct plain ModbusRegisterArrayImpl array from register values + * + * @param registerValues register values, each int corresponding to one register + * @return + */ + public ModbusRegisterArray(int... registerValues) { + this(registersFromValues(registerValues)); + } + /** * Return register at the given index * @@ -33,20 +69,33 @@ public interface ModbusRegisterArray extends Iterable { * @param index the index of the register to be returned. * @throws IndexOutOfBoundsException if the index is out of bounds. */ - ModbusRegister getRegister(int index); + public ModbusRegister getRegister(int index) { + return registers[index]; + } /** * Get number of registers stored in this instance * * @return */ - int size(); + public int size() { + return registers.length; + } + + @Override + public String toString() { + if (registers.length == 0) { + return "ModbusRegisterArrayImpl()"; + } + StringBuffer buffer = new StringBuffer(registers.length * 2).append("ModbusRegisterArrayImpl("); + return appendHexString(buffer).append(')').toString(); + } /** * Iterator over all the registers */ @Override - default Iterator iterator() { + public Iterator iterator() { return IntStream.range(0, size()).mapToObj(i -> getRegister(i)).iterator(); } @@ -57,7 +106,7 @@ default Iterator iterator() { * * @return string representing the bytes of the register array */ - default String toHexString() { + public String toHexString() { if (size() == 0) { return ""; } @@ -70,7 +119,7 @@ default String toHexString() { * Appends the register data as hex string to the given StringBuffer * */ - default StringBuffer appendHexString(StringBuffer buffer) { + public StringBuffer appendHexString(StringBuffer buffer) { IntStream.range(0, size()).forEachOrdered(index -> { getRegister(index).appendHexString(buffer); if (index < size() - 1) { diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRequestBlueprint.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRequestBlueprint.java deleted file mode 100644 index 096ce85c040e3..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusRequestBlueprint.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * 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.io.transport.modbus; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -import net.wimpi.modbus.Modbus; - -/** - * Base interface for Modbus requests - * - * @see ModbusReadRequestBlueprint - * @see ModbusWriteRequestBlueprint - * - * @author Sami Salonen - Initial contribution - * - */ -@NonNullByDefault -public interface ModbusRequestBlueprint { - - /** - * Returns the protocol identifier of this - * ModbusMessage as int.
- * The identifier is a 2-byte (short) non negative - * integer value valid in the range of 0-65535. - *

- * - * @return the protocol identifier as int. - */ - public default int getProtocolID() { - return Modbus.DEFAULT_PROTOCOL_ID; - } - - /** - * Returns the unit identifier of this - * ModbusMessage as int.
- * The identifier is a 1-byte non negative - * integer value valid in the range of 0-255. - *

- * - * @return the unit identifier as int. - */ - public int getUnitID(); - - /** - * Maximum number of tries to execute the request, when request fails - * - * For example, number 1 means on try only with no re-tries. - * - * @return number of maximum tries - */ - public int getMaxTries(); -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusCallback.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusResultCallback.java similarity index 93% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusCallback.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusResultCallback.java index 072f72f120003..eb0ca3e7b6929 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusCallback.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusResultCallback.java @@ -20,5 +20,5 @@ * @author Sami Salonen - Initial contribution */ @NonNullByDefault -public interface ModbusCallback { +public interface ModbusResultCallback { } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteCallback.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteCallback.java index 7003a483e3d7e..fdb83f47fdd78 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteCallback.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteCallback.java @@ -19,27 +19,14 @@ * * @author Sami Salonen - Initial contribution */ +@FunctionalInterface @NonNullByDefault -public interface ModbusWriteCallback extends ModbusCallback { +public interface ModbusWriteCallback extends ModbusResultCallback { /** - * Callback handler method for cases when an error occurred with write + * Callback handling response data * - * Note that only one of the two is called: onError, onResponse - * - * @request ModbusWriteRequestBlueprint representing the request - * @param Exception representing the issue with the request. Instance of - * {@link ModbusUnexpectedTransactionIdException} or {@link ModbusTransportException}. - */ - void onError(ModbusWriteRequestBlueprint request, Exception error); - - /** - * Callback handler method for successful writes - * - * Note that only one of the two is called: onError, onResponse - * - * @param request ModbusWriteRequestBlueprint representing the request - * @param response response matching the write request + * @param asyncModbusWriteResult result of the write operation */ - void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response); + void handle(AsyncModbusWriteResult result); } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteCoilRequestBlueprint.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteCoilRequestBlueprint.java index e7be48d19c1c1..505ce785cc1f3 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteCoilRequestBlueprint.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteCoilRequestBlueprint.java @@ -12,26 +12,110 @@ */ package org.openhab.io.transport.modbus; +import org.apache.commons.lang.builder.StandardToStringStyle; +import org.apache.commons.lang.builder.ToStringBuilder; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * Write request for coils + * Implementation for writing coils * * @author Sami Salonen - Initial contribution * */ @NonNullByDefault -public interface ModbusWriteCoilRequestBlueprint extends ModbusWriteRequestBlueprint { +public class ModbusWriteCoilRequestBlueprint extends ModbusWriteRequestBlueprint { + + private static StandardToStringStyle toStringStyle = new StandardToStringStyle(); + + static { + toStringStyle.setUseShortClassName(true); + } + + private final int slaveId; + private final int reference; + private final BitArray bits; + private final boolean writeMultiple; + private final int maxTries; + + /** + * Construct coil write request with single bit of data + * + * @param slaveId slave id to write to + * @param reference reference address + * @param data bit to write + * @param writeMultiple whether to use {@link ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS} over + * {@link ModbusWriteFunctionCode.WRITE_COIL} + * @param maxTries maximum number of tries in case of errors, should be at least 1 + */ + public ModbusWriteCoilRequestBlueprint(int slaveId, int reference, boolean data, boolean writeMultiple, + int maxTries) { + this(slaveId, reference, new BitArray(data), writeMultiple, maxTries); + } /** - * Coil data to write + * Construct coil write request with many bits of data * - * @return coils to write + * @param slaveId slave id to write to + * @param reference reference address + * @param data bit(s) to write + * @param writeMultiple whether to use {@link ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS} over + * {@link ModbusWriteFunctionCode.WRITE_COIL}. Useful with single bit of data. + * @param maxTries maximum number of tries in case of errors, should be at least 1 + * @throws IllegalArgumentException in case data is empty, writeMultiple is + * false but there are many bits to write. */ - public BitArray getCoils(); + public ModbusWriteCoilRequestBlueprint(int slaveId, int reference, BitArray data, boolean writeMultiple, + int maxTries) { + super(); + this.slaveId = slaveId; + this.reference = reference; + this.bits = data; + this.writeMultiple = writeMultiple; + this.maxTries = maxTries; + + if (!writeMultiple && bits.size() > 1) { + throw new IllegalArgumentException("With multiple coils, writeMultiple must be true"); + } + if (bits.size() == 0) { + throw new IllegalArgumentException("Must have at least one bit"); + } + if (maxTries <= 0) { + throw new IllegalArgumentException("maxTries should be positive, was " + maxTries); + } + } + + @Override + public int getUnitID() { + return slaveId; + } + + @Override + public int getReference() { + return reference; + } + + @Override + public ModbusWriteFunctionCode getFunctionCode() { + return writeMultiple ? ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS : ModbusWriteFunctionCode.WRITE_COIL; + } + + public BitArray getCoils() { + return bits; + } + + @Override + public int getMaxTries() { + return maxTries; + } + + @Override + public String toString() { + return new ToStringBuilder(this, toStringStyle).append("slaveId", slaveId).append("reference", reference) + .append("functionCode", getFunctionCode()).append("bits", bits).append("maxTries", maxTries).toString(); + } @Override - public default void accept(ModbusWriteRequestBlueprintVisitor visitor) { + public void accept(ModbusWriteRequestBlueprintVisitor visitor) { visitor.visit(this); } } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteRegisterRequestBlueprint.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteRegisterRequestBlueprint.java index 19fd1e5823007..83c2103760add 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteRegisterRequestBlueprint.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteRegisterRequestBlueprint.java @@ -12,21 +12,97 @@ */ package org.openhab.io.transport.modbus; +import org.apache.commons.lang.builder.StandardToStringStyle; +import org.apache.commons.lang.builder.ToStringBuilder; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * Write request for registers + * Implementation for writing registers * * @author Sami Salonen - Initial contribution * */ @NonNullByDefault -public interface ModbusWriteRegisterRequestBlueprint extends ModbusWriteRequestBlueprint { +public class ModbusWriteRegisterRequestBlueprint extends ModbusWriteRequestBlueprint { - public ModbusRegisterArray getRegisters(); + private static StandardToStringStyle toStringStyle = new StandardToStringStyle(); + + static { + toStringStyle.setUseShortClassName(true); + } + + private final int slaveId; + private final int reference; + private final ModbusRegisterArray registers; + private final boolean writeMultiple; + private final int maxTries; + + /** + * Construct coil write request with many bits of data + * + * @param slaveId slave id to write to + * @param reference reference address + * @param registers register(s) to write + * @param writeMultiple whether to use {@link ModbusWriteFunctionCode.WRITE_MULTIPLE_COILS} over + * {@link ModbusWriteFunctionCode.WRITE_COIL}. Useful with single register of data. + * @param maxTries maximum number of tries in case of errors, should be at least 1 + * @throws IllegalArgumentException in case data is empty, writeMultiple is + * false but there are many registers to write. + */ + public ModbusWriteRegisterRequestBlueprint(int slaveId, int reference, ModbusRegisterArray registers, + boolean writeMultiple, int maxTries) throws IllegalArgumentException { + super(); + this.slaveId = slaveId; + this.reference = reference; + this.registers = registers; + this.writeMultiple = writeMultiple; + this.maxTries = maxTries; + + if (!writeMultiple && registers.size() > 1) { + throw new IllegalArgumentException("With multiple registers, writeMultiple must be true"); + } + if (registers.size() == 0) { + throw new IllegalArgumentException("Must have at least one register"); + } + if (maxTries <= 0) { + throw new IllegalArgumentException("maxTries should be positive"); + } + } + + @Override + public int getReference() { + return reference; + } + + @Override + public int getUnitID() { + return slaveId; + } + + @Override + public ModbusWriteFunctionCode getFunctionCode() { + return writeMultiple ? ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS + : ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER; + } + + public ModbusRegisterArray getRegisters() { + return registers; + } + + @Override + public int getMaxTries() { + return maxTries; + } + + @Override + public String toString() { + return new ToStringBuilder(this, toStringStyle).append("slaveId", slaveId).append("reference", reference) + .append("functionCode", getFunctionCode()).append("registers", registers).append("maxTries", maxTries) + .toString(); + } @Override - public default void accept(ModbusWriteRequestBlueprintVisitor visitor) { + public void accept(ModbusWriteRequestBlueprintVisitor visitor) { visitor.visit(this); } } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteRequestBlueprint.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteRequestBlueprint.java index f79840c32765a..5cab106286150 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteRequestBlueprint.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusWriteRequestBlueprint.java @@ -23,7 +23,7 @@ * */ @NonNullByDefault -public interface ModbusWriteRequestBlueprint extends ModbusRequestBlueprint { +public abstract class ModbusWriteRequestBlueprint { /** * Returns the protocol identifier of this @@ -34,8 +34,7 @@ public interface ModbusWriteRequestBlueprint extends ModbusRequestBlueprint { * * @return the protocol identifier as int. */ - @Override - public default int getProtocolID() { + public int getProtocolID() { return Modbus.DEFAULT_PROTOCOL_ID; } @@ -47,7 +46,7 @@ public default int getProtocolID() { * @return the reference of the register * to start reading from as int. */ - public int getReference(); + public abstract int getReference(); /** * Returns the unit identifier of this @@ -58,8 +57,7 @@ public default int getProtocolID() { * * @return the unit identifier as int. */ - @Override - public int getUnitID(); + public abstract int getUnitID(); /** * Returns the function code of this @@ -75,18 +73,17 @@ public default int getProtocolID() { * * @see net.wimpi.modbus.Modbus */ - public ModbusWriteFunctionCode getFunctionCode(); + public abstract ModbusWriteFunctionCode getFunctionCode(); /** * Get maximum number of tries, in case errors occur. Should be at least 1. */ - @Override - public int getMaxTries(); + public abstract int getMaxTries(); /** * Accept visitor * * @param visitor */ - public void accept(ModbusWriteRequestBlueprintVisitor visitor); + public abstract void accept(ModbusWriteRequestBlueprintVisitor visitor); } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/PollTask.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/PollTask.java index 36754ad692dd1..bda4ff5737ec7 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/PollTask.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/PollTask.java @@ -25,7 +25,8 @@ * @see ModbusManager.registerRegularPoll */ @NonNullByDefault -public interface PollTask extends TaskWithEndpoint { +public interface PollTask extends + TaskWithEndpoint> { @Override default int getMaxTries() { return getRequest().getMaxTries(); diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/TaskWithEndpoint.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/TaskWithEndpoint.java index 6a45658044752..ef36691d37c40 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/TaskWithEndpoint.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/TaskWithEndpoint.java @@ -13,7 +13,6 @@ package org.openhab.io.transport.modbus; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; /** @@ -25,7 +24,7 @@ * @param callback type */ @NonNullByDefault -public interface TaskWithEndpoint { +public interface TaskWithEndpoint> { /** * Gets endpoint associated with this task * @@ -41,12 +40,18 @@ public interface TaskWithEndpoint { R getRequest(); /** - * Gets callback associated with this task, will be called with response + * Gets the result callback associated with this task, will be called with response * * @return */ - @Nullable - C getCallback(); + C getResultCallback(); + + /** + * Gets the failure callback associated with this task, will be called in case of an error + * + * @return + */ + F getFailureCallback(); int getMaxTries(); } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/WriteTask.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/WriteTask.java index 05611fa390708..079e053e29e3b 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/WriteTask.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/WriteTask.java @@ -23,7 +23,8 @@ * */ @NonNullByDefault -public interface WriteTask extends TaskWithEndpoint { +public interface WriteTask extends + TaskWithEndpoint> { @Override default int getMaxTries() { return getRequest().getMaxTries(); diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusConnectionException.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusConnectionException.java similarity index 96% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusConnectionException.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusConnectionException.java index f828b0368ee72..ebd6c6ec00cb5 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusConnectionException.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusConnectionException.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.io.transport.modbus; +package org.openhab.io.transport.modbus.exception; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusSlaveErrorResponseException.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusSlaveErrorResponseException.java similarity index 98% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusSlaveErrorResponseException.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusSlaveErrorResponseException.java index d2f89d15ac9b8..bccd7f405a9b8 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusSlaveErrorResponseException.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusSlaveErrorResponseException.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.io.transport.modbus; +package org.openhab.io.transport.modbus.exception; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusSlaveIOException.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusSlaveIOException.java similarity index 93% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusSlaveIOException.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusSlaveIOException.java index 8ea22f7a6de57..e1c377ed1c25c 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusSlaveIOException.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusSlaveIOException.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.io.transport.modbus; +package org.openhab.io.transport.modbus.exception; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusTransportException.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusTransportException.java similarity index 93% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusTransportException.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusTransportException.java index 9f9c0e78ef196..5c485ee521abc 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusTransportException.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusTransportException.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.io.transport.modbus; +package org.openhab.io.transport.modbus.exception; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusUnexpectedResponseFunctionCodeException.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusUnexpectedResponseFunctionCodeException.java similarity index 96% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusUnexpectedResponseFunctionCodeException.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusUnexpectedResponseFunctionCodeException.java index b4000cc8496b7..7f472f7a5aa06 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusUnexpectedResponseFunctionCodeException.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusUnexpectedResponseFunctionCodeException.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.io.transport.modbus; +package org.openhab.io.transport.modbus.exception; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusUnexpectedResponseSizeException.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusUnexpectedResponseSizeException.java similarity index 96% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusUnexpectedResponseSizeException.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusUnexpectedResponseSizeException.java index 62d0101dd34ab..16ba3adc59fc2 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusUnexpectedResponseSizeException.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusUnexpectedResponseSizeException.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.io.transport.modbus; +package org.openhab.io.transport.modbus.exception; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusUnexpectedTransactionIdException.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusUnexpectedTransactionIdException.java similarity index 96% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusUnexpectedTransactionIdException.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusUnexpectedTransactionIdException.java index 183a001198d44..312c9fdfd7d3e 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/ModbusUnexpectedTransactionIdException.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/exception/ModbusUnexpectedTransactionIdException.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.io.transport.modbus; +package org.openhab.io.transport.modbus.exception; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicPollTaskImpl.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BasicPollTask.java similarity index 61% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicPollTaskImpl.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BasicPollTask.java index b436d72169506..162bda1a7537f 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicPollTaskImpl.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BasicPollTask.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.io.transport.modbus; +package org.openhab.io.transport.modbus.internal; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; @@ -18,6 +18,10 @@ import org.apache.commons.lang.builder.ToStringBuilder; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +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.PollTask; import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; /** @@ -32,7 +36,7 @@ * */ @NonNullByDefault -public class BasicPollTaskImpl implements PollTask { +public class BasicPollTask implements PollTask { static StandardToStringStyle toStringStyle = new StandardToStringStyle(); static { @@ -40,18 +44,16 @@ public class BasicPollTaskImpl implements PollTask { } private ModbusSlaveEndpoint endpoint; - private BasicModbusReadRequestBlueprint request; - private @Nullable ModbusReadCallback callback; + private ModbusReadRequestBlueprint request; + private ModbusReadCallback resultCallback; + private ModbusFailureCallback failureCallback; - public BasicPollTaskImpl(ModbusSlaveEndpoint endpoint, BasicModbusReadRequestBlueprint request) { - this(endpoint, request, null); - } - - public BasicPollTaskImpl(ModbusSlaveEndpoint endpoint, BasicModbusReadRequestBlueprint request, - @Nullable ModbusReadCallback callback) { + public BasicPollTask(ModbusSlaveEndpoint endpoint, ModbusReadRequestBlueprint request, + ModbusReadCallback resultCallback, ModbusFailureCallback failureCallback) { this.endpoint = endpoint; this.request = request; - this.callback = callback; + this.resultCallback = resultCallback; + this.failureCallback = failureCallback; } @Override @@ -65,19 +67,26 @@ public ModbusSlaveEndpoint getEndpoint() { } @Override - public @Nullable ModbusReadCallback getCallback() { - return callback; + public ModbusReadCallback getResultCallback() { + return resultCallback; + } + + @Override + public ModbusFailureCallback getFailureCallback() { + return failureCallback; } @Override public int hashCode() { - return new HashCodeBuilder(69, 5).append(request).append(getEndpoint()).append(getCallback()).toHashCode(); + return new HashCodeBuilder(69, 5).append(request).append(getEndpoint()).append(getResultCallback()) + .append(getFailureCallback()).toHashCode(); } @Override public String toString() { return new ToStringBuilder(this, toStringStyle).append("request", request).append("endpoint", endpoint) - .append("callback", getCallback()).toString(); + .append("resultCallback", getResultCallback()).append("failureCallback", getFailureCallback()) + .toString(); } @Override @@ -91,8 +100,9 @@ public boolean equals(@Nullable Object obj) { if (obj.getClass() != getClass()) { return false; } - BasicPollTaskImpl rhs = (BasicPollTaskImpl) obj; + BasicPollTask rhs = (BasicPollTask) obj; return new EqualsBuilder().append(request, rhs.request).append(endpoint, rhs.endpoint) - .append(getCallback(), rhs.getCallback()).isEquals(); + .append(getResultCallback(), rhs.getResultCallback()) + .append(getFailureCallback(), rhs.getFailureCallback()).isEquals(); } } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicWriteTask.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BasicWriteTask.java similarity index 62% rename from bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicWriteTask.java rename to bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BasicWriteTask.java index 148a4d56cee7d..5d4c870070216 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/BasicWriteTask.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BasicWriteTask.java @@ -10,12 +10,15 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.io.transport.modbus; +package org.openhab.io.transport.modbus.internal; import org.apache.commons.lang.builder.StandardToStringStyle; import org.apache.commons.lang.builder.ToStringBuilder; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; +import org.openhab.io.transport.modbus.ModbusFailureCallback; +import org.openhab.io.transport.modbus.ModbusWriteCallback; +import org.openhab.io.transport.modbus.ModbusWriteRequestBlueprint; +import org.openhab.io.transport.modbus.WriteTask; import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; /** @@ -34,14 +37,16 @@ public class BasicWriteTask implements WriteTask { private ModbusSlaveEndpoint endpoint; private ModbusWriteRequestBlueprint request; - private ModbusWriteCallback callback; + private ModbusWriteCallback resultCallback; + private ModbusFailureCallback failureCallback; public BasicWriteTask(ModbusSlaveEndpoint endpoint, ModbusWriteRequestBlueprint request, - ModbusWriteCallback callback) { + ModbusWriteCallback resultCallback, ModbusFailureCallback failureCallback) { super(); this.endpoint = endpoint; this.request = request; - this.callback = callback; + this.resultCallback = resultCallback; + this.failureCallback = failureCallback; } @Override @@ -55,13 +60,18 @@ public ModbusWriteRequestBlueprint getRequest() { } @Override - public @Nullable ModbusWriteCallback getCallback() { - return callback; + public ModbusWriteCallback getResultCallback() { + return resultCallback; + } + + @Override + public ModbusFailureCallback getFailureCallback() { + return failureCallback; } @Override public String toString() { return new ToStringBuilder(this, TO_STRING_STYLE).append("request", request).append("endpoint", endpoint) - .append("callback", getCallback()).toString(); + .append("resultCallback", resultCallback).append("failureCallback", failureCallback).toString(); } } diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BitArrayWrappingBitVector.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BitArrayWrappingBitVector.java deleted file mode 100644 index ffc89bcec71bc..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/BitArrayWrappingBitVector.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * 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.io.transport.modbus.internal; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.io.transport.modbus.BitArray; - -import net.wimpi.modbus.util.BitVector; - -/** - * BitArray implementation which wraps {@link BitVector} - * - * @author Sami Salonen - Initial contribution - */ -@NonNullByDefault -public class BitArrayWrappingBitVector implements BitArray { - - private BitVector wrapped; - private int safeSize; - - /** - * Construct instance using BitVector data - * - * Depending how the wrapped data is constructed, its size might not be correct (bug of jamod library, as of - * 2017-10). Due to this reason, safeSize is provided to check out-of-bounds situations - * - * @param wrapped wrapped data - * @param safeSize size of wrapped data - */ - public BitArrayWrappingBitVector(BitVector wrapped, int safeSize) { - this.wrapped = wrapped; - this.safeSize = safeSize; - } - - @Override - public boolean getBit(int index) { - if (index >= size()) { - throw new IndexOutOfBoundsException(); - } - return this.wrapped.getBit(index); - } - - @Override - public int size() { - return safeSize; - } - - @Override - public String toString() { - return new StringBuilder("BitArrayWrappingBitVector(bits=").append(safeSize == 0 ? "" : toBinaryString()) - .append(")").toString(); - } - - @Override - public boolean equals(@Nullable Object obj) { - return sizeAndValuesEquals(obj); - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusConnectionPool.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusConnectionPool.java index aecd80d9eb42a..4725addd6edff 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusConnectionPool.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusConnectionPool.java @@ -34,8 +34,7 @@ public class ModbusConnectionPool extends GenericKeyedObjectPool { // policy is set in super constructor via setConfig - @NonNullByDefault({}) - private volatile EvictionPolicy policy; + private volatile @NonNullByDefault({}) EvictionPolicy policy; public ModbusConnectionPool(KeyedPooledObjectFactory factory) { super(factory, new ModbusPoolConfig()); diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusLibraryWrapper.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusLibraryWrapper.java index 0cea5684f06f9..2c47fdd95cd50 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusLibraryWrapper.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusLibraryWrapper.java @@ -19,10 +19,12 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.io.transport.modbus.AsyncModbusReadResult; import org.openhab.io.transport.modbus.BitArray; import org.openhab.io.transport.modbus.ModbusReadCallback; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; +import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.ModbusWriteCoilRequestBlueprint; import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint; @@ -58,6 +60,7 @@ import net.wimpi.modbus.net.SerialConnection; import net.wimpi.modbus.net.TCPMasterConnection; import net.wimpi.modbus.net.UDPMasterConnection; +import net.wimpi.modbus.procimg.InputRegister; import net.wimpi.modbus.procimg.Register; import net.wimpi.modbus.procimg.SimpleInputRegister; import net.wimpi.modbus.util.BitVector; @@ -75,6 +78,22 @@ private static Logger getLogger() { return LoggerFactory.getLogger(ModbusLibraryWrapper.class); } + private static BitArray bitArrayFromBitVector(BitVector bitVector, int count) { + boolean[] bits = new boolean[count]; + for (int i = 0; i < count; i++) { + bits[i] = bitVector.getBit(i); + } + return new BitArray(bits); + } + + private static ModbusRegisterArray modbusRegisterArrayFromInputRegisters(InputRegister[] inputRegisters) { + ModbusRegister[] registers = new ModbusRegister[inputRegisters.length]; + for (int i = 0; i < inputRegisters.length; i++) { + registers[i] = new ModbusRegister(inputRegisters[i].getValue()); + } + return new ModbusRegisterArray(registers); + } + /** * Convert the general request to Modbus library request object * @@ -303,18 +322,20 @@ public static void invokeCallbackWithResponse(ModbusReadRequestBlueprint request int dataItemsInResponse = getNumberOfItemsInResponse(response, request); if (request.getFunctionCode() == ModbusReadFunctionCode.READ_COILS) { BitVector bits = ((ReadCoilsResponse) response).getCoils(); - callback.onBits(request, - new BitArrayWrappingBitVector(bits, Math.min(dataItemsInResponse, request.getDataLength()))); + BitArray payload = bitArrayFromBitVector(bits, Math.min(dataItemsInResponse, request.getDataLength())); + callback.handle(new AsyncModbusReadResult(request, payload)); } else if (request.getFunctionCode() == ModbusReadFunctionCode.READ_INPUT_DISCRETES) { BitVector bits = ((ReadInputDiscretesResponse) response).getDiscretes(); - callback.onBits(request, - new BitArrayWrappingBitVector(bits, Math.min(dataItemsInResponse, request.getDataLength()))); + BitArray payload = bitArrayFromBitVector(bits, Math.min(dataItemsInResponse, request.getDataLength())); + callback.handle(new AsyncModbusReadResult(request, payload)); } else if (request.getFunctionCode() == ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS) { - callback.onRegisters(request, new RegisterArrayWrappingInputRegister( - ((ReadMultipleRegistersResponse) response).getRegisters())); + ModbusRegisterArray payload = modbusRegisterArrayFromInputRegisters( + ((ReadMultipleRegistersResponse) response).getRegisters()); + callback.handle(new AsyncModbusReadResult(request, payload)); } else if (request.getFunctionCode() == ModbusReadFunctionCode.READ_INPUT_REGISTERS) { - callback.onRegisters(request, - new RegisterArrayWrappingInputRegister(((ReadInputRegistersResponse) response).getRegisters())); + ModbusRegisterArray payload = modbusRegisterArrayFromInputRegisters( + ((ReadInputRegistersResponse) response).getRegisters()); + callback.handle(new AsyncModbusReadResult(request, payload)); } else { throw new IllegalArgumentException( String.format("Unexpected function code %s", request.getFunctionCode())); diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusManagerImpl.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusManagerImpl.java index 06136ab6e59e9..4032278666fca 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusManagerImpl.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusManagerImpl.java @@ -13,13 +13,13 @@ package org.openhab.io.transport.modbus.internal; import java.io.IOException; -import java.util.Collection; +import java.util.LinkedList; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadPoolExecutor; @@ -34,17 +34,16 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.util.ConcurrentHashSet; import org.eclipse.smarthome.core.common.ThreadPoolManager; -import org.openhab.io.transport.modbus.ModbusCallback; -import org.openhab.io.transport.modbus.ModbusConnectionException; +import org.openhab.io.transport.modbus.AsyncModbusFailure; +import org.openhab.io.transport.modbus.AsyncModbusWriteResult; +import org.openhab.io.transport.modbus.ModbusCommunicationInterface; +import org.openhab.io.transport.modbus.ModbusFailureCallback; import org.openhab.io.transport.modbus.ModbusManager; -import org.openhab.io.transport.modbus.ModbusManagerListener; import org.openhab.io.transport.modbus.ModbusReadCallback; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.ModbusRequestBlueprint; -import org.openhab.io.transport.modbus.ModbusUnexpectedResponseFunctionCodeException; -import org.openhab.io.transport.modbus.ModbusUnexpectedResponseSizeException; -import org.openhab.io.transport.modbus.ModbusUnexpectedTransactionIdException; +import org.openhab.io.transport.modbus.ModbusResultCallback; import org.openhab.io.transport.modbus.ModbusWriteCallback; import org.openhab.io.transport.modbus.ModbusWriteRequestBlueprint; import org.openhab.io.transport.modbus.PollTask; @@ -56,6 +55,10 @@ import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpointVisitor; import org.openhab.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint; import org.openhab.io.transport.modbus.endpoint.ModbusUDPSlaveEndpoint; +import org.openhab.io.transport.modbus.exception.ModbusConnectionException; +import org.openhab.io.transport.modbus.exception.ModbusUnexpectedResponseFunctionCodeException; +import org.openhab.io.transport.modbus.exception.ModbusUnexpectedResponseSizeException; +import org.openhab.io.transport.modbus.exception.ModbusUnexpectedTransactionIdException; import org.openhab.io.transport.modbus.internal.pooling.ModbusSlaveConnectionFactoryImpl; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -191,7 +194,7 @@ public void accept(AggregateStopWatch timer, PollTask task, ModbusSlaveConnectio ModbusUnexpectedResponseFunctionCodeException, ModbusUnexpectedResponseSizeException { ModbusSlaveEndpoint endpoint = task.getEndpoint(); ModbusReadRequestBlueprint request = task.getRequest(); - ModbusReadCallback callback = task.getCallback(); + ModbusReadCallback callback = task.getResultCallback(); String operationId = timer.operationId; ModbusTransaction transaction = ModbusLibraryWrapper.createTransactionForEndpoint(endpoint, connection); @@ -209,10 +212,8 @@ public void accept(AggregateStopWatch timer, PollTask task, ModbusSlaveConnectio checkTransactionId(response, libRequest, operationId); checkFunctionCode(response, libRequest, operationId); checkResponseSize(response, request, operationId); - if (callback != null) { - timer.callback.timeRunnable( - () -> ModbusLibraryWrapper.invokeCallbackWithResponse(request, callback, response)); - } + timer.callback + .timeRunnable(() -> ModbusLibraryWrapper.invokeCallbackWithResponse(request, callback, response)); } } @@ -229,7 +230,8 @@ public void accept(AggregateStopWatch timer, WriteTask task, ModbusSlaveConnecti ModbusUnexpectedResponseFunctionCodeException { ModbusSlaveEndpoint endpoint = task.getEndpoint(); ModbusWriteRequestBlueprint request = task.getRequest(); - ModbusWriteCallback callback = task.getCallback(); + @Nullable + ModbusWriteCallback callback = task.getResultCallback(); String operationId = timer.operationId; ModbusTransaction transaction = ModbusLibraryWrapper.createTransactionForEndpoint(endpoint, connection); @@ -247,10 +249,8 @@ public void accept(AggregateStopWatch timer, WriteTask task, ModbusSlaveConnecti response.getFunctionCode(), response.getTransactionID(), response.getHexMessage(), operationId); checkTransactionId(response, libRequest, operationId); checkFunctionCode(response, libRequest, operationId); - if (callback != null) { - timer.callback.timeRunnable( - () -> invokeCallbackWithResponse(request, callback, new ModbusResponseImpl(response))); - } + timer.callback.timeRunnable( + () -> invokeCallbackWithResponse(request, callback, new ModbusResponseImpl(response))); } } @@ -308,19 +308,16 @@ public void accept(AggregateStopWatch timer, WriteTask task, ModbusSlaveConnecti * - https://community.openhab.org/t/modbus-connection-problem/6108/ * - https://community.openhab.org/t/connection-pooling-in-modbus-binding/5246/ */ - @Nullable - private volatile KeyedObjectPool connectionPool; - @Nullable - private volatile ModbusSlaveConnectionFactoryImpl connectionFactory; + + private volatile @Nullable KeyedObjectPool connectionPool; + private volatile @Nullable ModbusSlaveConnectionFactoryImpl connectionFactory; private volatile Map> scheduledPollTasks = new ConcurrentHashMap<>(); /** * Executor for requests */ - @Nullable - private volatile ScheduledExecutorService scheduledThreadPoolExecutor; - private volatile Collection listeners = new CopyOnWriteArraySet<>(); - @Nullable - private volatile ScheduledFuture monitorFuture; + private volatile @Nullable ScheduledExecutorService scheduledThreadPoolExecutor; + private volatile @Nullable ScheduledFuture monitorFuture; + private volatile Set communicationInterfaces = new ConcurrentHashSet<>(); private void constructConnectionPool() { ModbusSlaveConnectionFactoryImpl connectionFactory = new ModbusSlaveConnectionFactoryImpl(); @@ -362,7 +359,7 @@ private void constructConnectionPool() { @SuppressWarnings("null") @Override public void onSwallowException(@Nullable Exception e) { - LoggerFactory.getLogger(ModbusManagerImpl.class).error( + LoggerFactory.getLogger(ModbusManagerImpl.class).warn( "Connection pool swallowed unexpected exception:{} {}", Optional.ofNullable(e).map(ex -> ex.getClass().getSimpleName()).orElse(""), Optional.ofNullable(e).map(ex -> ex.getMessage()).orElse(""), e); @@ -447,7 +444,7 @@ private void returnConnection(ModbusSlaveEndpoint endpoint, Optional> Optional getConnection( + private , T extends TaskWithEndpoint> Optional getConnection( AggregateStopWatch timer, boolean oneOffTask, @NonNull T task) throws PollTaskUnregistered { KeyedObjectPool connectionPool = this.connectionPool; if (connectionPool == null) { @@ -459,10 +456,10 @@ private failureCallback = task.getFailureCallback(); ModbusSlaveEndpoint endpoint = task.getEndpoint(); - ModbusRequestBlueprint request = task.getRequest(); + R request = task.getRequest(); Optional connection = timer.connection.timeSupplier(() -> borrowConnection(endpoint)); logger.trace("Executing task {} (oneOff={})! Connection received in {} ms [operation ID {}]", task, oneOffTask, System.currentTimeMillis() - connectionBorrowStart, operationId); @@ -474,26 +471,17 @@ private invokeCallbackWithError(request, callback, new ModbusConnectionException(endpoint))); - } + timer.callback.timeRunnable( + () -> invokeCallbackWithError(request, failureCallback, new ModbusConnectionException(endpoint))); } return connection; } - private void invokeCallbackWithError(ModbusRequestBlueprint request, ModbusCallback callback, Exception error) { + private void invokeCallbackWithError(R request, ModbusFailureCallback callback, Exception error) { try { - logger.trace("Calling write response callback {} for request {}. Error was {} {}", callback, request, + logger.trace("Calling error response callback {} for request {}. Error was {} {}", callback, request, error.getClass().getName(), error.getMessage()); - if (request instanceof ModbusReadRequestBlueprint) { - ((ModbusReadCallback) callback).onError((ModbusReadRequestBlueprint) request, error); - } else if (request instanceof ModbusWriteRequestBlueprint) { - ((ModbusWriteCallback) callback).onError((ModbusWriteRequestBlueprint) request, error); - } else { - throw new IllegalStateException(String.format("Request %s or callback %s is of wrong type.", - request.getClass().getName(), callback.getClass().getName())); - } + callback.handle(new AsyncModbusFailure(request, error)); } finally { logger.trace("Called write response callback {} for request {}. Error was {} {}", callback, request, error.getClass().getName(), error.getMessage()); @@ -505,7 +493,7 @@ private void invokeCallbackWithResponse(ModbusWriteRequestBlueprint request, Mod try { logger.trace("Calling write response callback {} for request {}. Response was {}", callback, request, response); - callback.onWriteResponse(request, response); + callback.handle(new AsyncModbusWriteResult(request, response)); } finally { logger.trace("Called write response callback {} for request {}. Response was {}", callback, request, response); @@ -534,8 +522,8 @@ private void verifyTaskIsRegistered(PollTask task) throws PollTaskUnregistered { * @param oneOffTask * @param operation */ - private > void executeOperation( - @NonNull T task, boolean oneOffTask, ModbusOperation operation) { + private , T extends TaskWithEndpoint> void executeOperation( + T task, boolean oneOffTask, ModbusOperation operation) { AggregateStopWatch timer = new AggregateStopWatch(); timer.total.resume(); String operationId = timer.operationId; @@ -550,10 +538,10 @@ private lastError = new AtomicReference<>(); + @SuppressWarnings("null") // since cfg in lambda cannot be really null long retryDelay = Optional.ofNullable(connectionFactory.getEndpointPoolConfiguration(endpoint)) .map(cfg -> cfg.getInterTransactionDelayMillis()).orElse(0L); @@ -712,11 +700,9 @@ private { - invokeCallbackWithError(request, callback, exception); - }); - } + timer.callback.timeRunnable(() -> { + invokeCallbackWithError(request, failureCallback, exception); + }); } } catch (PollTaskUnregistered e) { logger.warn("Poll task was unregistered -- not executing/proceeding with the poll: {} [operation ID {}]", @@ -736,123 +722,171 @@ private submitOneTimePoll(PollTask task) { - ScheduledExecutorService executor = scheduledThreadPoolExecutor; - Objects.requireNonNull(executor, "Not activated!"); - long scheduleTime = System.currentTimeMillis(); - logger.debug("Scheduling one-off poll task {}", task); - ScheduledFuture future = executor.schedule(() -> { - long millisInThreadPoolWaiting = System.currentTimeMillis() - scheduleTime; - logger.debug("Will now execute one-off poll task {}, waited in thread pool for {}", task, - millisInThreadPoolWaiting); - executeOperation(task, true, pollOperation); - }, 0L, TimeUnit.MILLISECONDS); - return future; - } + private class ModbusCommunicationInterfaceImpl implements ModbusCommunicationInterface { - @Override - public void registerRegularPoll(@NonNull PollTask task, long pollPeriodMillis, long initialDelayMillis) { - synchronized (this) { - ScheduledExecutorService executor = scheduledThreadPoolExecutor; - Objects.requireNonNull(executor, "Not activated!"); - logger.trace("Registering poll task {} with period {} using initial delay {}", task, pollPeriodMillis, - initialDelayMillis); - if (scheduledPollTasks.containsKey(task)) { - logger.trace("Unregistering previous poll task (possibly with different period)"); - unregisterRegularPoll(task); - } - ScheduledFuture future = executor.scheduleWithFixedDelay(() -> { - long started = System.currentTimeMillis(); - logger.debug("Executing scheduled ({}ms) poll task {}. Current millis: {}", pollPeriodMillis, task, - started); - try { - executeOperation(task, false, pollOperation); - } catch (RuntimeException e) { - // We want to catch all unexpected exceptions since all unhandled exceptions make - // ScheduledExecutorService halt the polling. It is better to print out the exception, and try again - // (on next poll cycle) - logger.warn( - "Execution of scheduled ({}ms) poll task {} failed unexpectedly. Ignoring exception, polling again according to poll interval.", - pollPeriodMillis, task, e); - } - long finished = System.currentTimeMillis(); - logger.debug( - "Execution of scheduled ({}ms) poll task {} finished at {}. Was started at millis: {} (=duration of {} millis)", - pollPeriodMillis, task, finished, started, finished - started); - }, initialDelayMillis, pollPeriodMillis, TimeUnit.MILLISECONDS); + private volatile ModbusSlaveEndpoint endpoint; + private volatile Set pollTasksRegisteredByThisCommInterface = new ConcurrentHashSet<>(); + private volatile boolean closed; + private @Nullable EndpointPoolConfiguration configuration; - scheduledPollTasks.put(task, future); - logger.trace("Registered poll task {} with period {} using initial delay {}", task, pollPeriodMillis, - initialDelayMillis); + @SuppressWarnings("null") + public ModbusCommunicationInterfaceImpl(ModbusSlaveEndpoint endpoint, + @Nullable EndpointPoolConfiguration configuration) { + this.endpoint = endpoint; + this.configuration = configuration; + connectionFactory.setEndpointPoolConfiguration(endpoint, configuration); } - } - @SuppressWarnings({ "null", "unused" }) - @Override - public boolean unregisterRegularPoll(PollTask task) { - synchronized (this) { - ScheduledExecutorService executor = this.scheduledThreadPoolExecutor; - ModbusSlaveConnectionFactoryImpl factory = this.connectionFactory; + @Override + public Future submitOneTimePoll(ModbusReadRequestBlueprint request, ModbusReadCallback resultCallback, + ModbusFailureCallback failureCallback) { + if (closed) { + throw new IllegalStateException("Communication interface is closed already!"); + } + ScheduledExecutorService executor = scheduledThreadPoolExecutor; Objects.requireNonNull(executor, "Not activated!"); - Objects.requireNonNull(factory, "Not activated!"); + long scheduleTime = System.currentTimeMillis(); + BasicPollTask task = new BasicPollTask(endpoint, request, resultCallback, failureCallback); + logger.debug("Scheduling one-off poll task {}", task); + Future future = executor.submit(() -> { + long millisInThreadPoolWaiting = System.currentTimeMillis() - scheduleTime; + logger.debug("Will now execute one-off poll task {}, waited in thread pool for {}", task, + millisInThreadPoolWaiting); + executeOperation(task, true, pollOperation); + }); + return future; + } - // cancel poller - @Nullable - ScheduledFuture future = scheduledPollTasks.remove(task); - if (future == null) { - // No such poll task - logger.warn("Caller tried to unregister nonexisting poll task {}", task); - return false; + @Override + public PollTask registerRegularPoll(ModbusReadRequestBlueprint request, long pollPeriodMillis, + long initialDelayMillis, ModbusReadCallback resultCallback, + ModbusFailureCallback failureCallback) { + synchronized (ModbusManagerImpl.this) { + if (closed) { + throw new IllegalStateException("Communication interface is closed already!"); + } + ScheduledExecutorService executor = scheduledThreadPoolExecutor; + Objects.requireNonNull(executor, "Not activated!"); + BasicPollTask task = new BasicPollTask(endpoint, request, resultCallback, failureCallback); + logger.trace("Registering poll task {} with period {} using initial delay {}", task, pollPeriodMillis, + initialDelayMillis); + if (scheduledPollTasks.containsKey(task)) { + logger.trace("Unregistering previous poll task (possibly with different period)"); + unregisterRegularPoll(task); + } + ScheduledFuture future = executor.scheduleWithFixedDelay(() -> { + long started = System.currentTimeMillis(); + logger.debug("Executing scheduled ({}ms) poll task {}. Current millis: {}", pollPeriodMillis, task, + started); + try { + executeOperation(task, false, pollOperation); + } catch (RuntimeException e) { + // We want to catch all unexpected exceptions since all unhandled exceptions make + // ScheduledExecutorService halt the polling. It is better to print out the exception, and try + // again + // (on next poll cycle) + logger.warn( + "Execution of scheduled ({}ms) poll task {} failed unexpectedly. Ignoring exception, polling again according to poll interval.", + pollPeriodMillis, task, e); + } + long finished = System.currentTimeMillis(); + logger.debug( + "Execution of scheduled ({}ms) poll task {} finished at {}. Was started at millis: {} (=duration of {} millis)", + pollPeriodMillis, task, finished, started, finished - started); + }, initialDelayMillis, pollPeriodMillis, TimeUnit.MILLISECONDS); + + scheduledPollTasks.put(task, future); + pollTasksRegisteredByThisCommInterface.add(task); + logger.trace("Registered poll task {} with period {} using initial delay {}", task, pollPeriodMillis, + initialDelayMillis); + return task; } - logger.info("Unregistering regular poll task {} (interrupting if necessary)", task); - - // Make sure connections to this endpoint are closed when they are returned to pool (which - // is usually pretty soon as transactions should be relatively short-lived) - factory.disconnectOnReturn(task.getEndpoint(), System.currentTimeMillis()); + } - future.cancel(true); + @SuppressWarnings({ "null", "unused" }) + @Override + public boolean unregisterRegularPoll(PollTask task) { + synchronized (ModbusManagerImpl.this) { + if (closed) { + throw new IllegalStateException("Communication interface is closed already!"); + } + pollTasksRegisteredByThisCommInterface.remove(task); + ModbusSlaveConnectionFactoryImpl localConnectionFactory = connectionFactory; + Objects.requireNonNull(localConnectionFactory, "Not activated!"); + + // cancel poller + @Nullable + ScheduledFuture future = scheduledPollTasks.remove(task); + if (future == null) { + // No such poll task + logger.warn("Caller tried to unregister nonexisting poll task {}", task); + return false; + } + logger.debug("Unregistering regular poll task {} (interrupting if necessary)", task); + future.cancel(true); + logger.debug("Poll task {} canceled", task); + return true; + } + } - logger.info("Poll task {} canceled", task); + @Override + public Future submitOneTimeWrite(ModbusWriteRequestBlueprint request, ModbusWriteCallback resultCallback, + ModbusFailureCallback failureCallback) { + if (closed) { + throw new IllegalStateException("Communication interface is closed already!"); + } + ScheduledExecutorService localScheduledThreadPoolExecutor = scheduledThreadPoolExecutor; + Objects.requireNonNull(localScheduledThreadPoolExecutor, "Not activated!"); + WriteTask task = new BasicWriteTask(endpoint, request, resultCallback, failureCallback); + long scheduleTime = System.currentTimeMillis(); + logger.debug("Scheduling one-off write task {}", task); + Future future = localScheduledThreadPoolExecutor.submit(() -> { + long millisInThreadPoolWaiting = System.currentTimeMillis() - scheduleTime; + logger.debug("Will now execute one-off write task {}, waited in thread pool for {}", task, + millisInThreadPoolWaiting); + executeOperation(task, true, writeOperation); + }); + return future; + } - try { - // Close all idle connections as well (they will be reconnected if necessary on borrow) - if (connectionPool != null) { - connectionPool.clear(task.getEndpoint()); + @Override + public void close() throws Exception { + synchronized (ModbusManagerImpl.this) { + if (closed) { + throw new IllegalStateException("Communication interface is closed already!"); } - } catch (Exception e) { - logger.error("Could not clear poll task {} endpoint {}. Stack trace follows", task, task.getEndpoint(), - e); - return false; + // Iterate over all tasks registered by this communication interface, and unregister those + // We copy pollTasksRegisteredByThisCommInterface temporarily so that unregisterRegularPoll can + // remove entries from pollTasksRegisteredByThisCommInterface + Iterable tasksToUnregister = new LinkedList<>(pollTasksRegisteredByThisCommInterface); + for (PollTask task : tasksToUnregister) { + unregisterRegularPoll(task); + } + unregisterCommunicationInterface(this); + closed = true; } - - return true; } - } - @Override - public ScheduledFuture submitOneTimeWrite(WriteTask task) { - ScheduledExecutorService scheduledThreadPoolExecutor = this.scheduledThreadPoolExecutor; - Objects.requireNonNull(scheduledThreadPoolExecutor, "Not activated!"); - long scheduleTime = System.currentTimeMillis(); - logger.debug("Scheduling one-off write task {}", task); - ScheduledFuture future = scheduledThreadPoolExecutor.schedule(() -> { - long millisInThreadPoolWaiting = System.currentTimeMillis() - scheduleTime; - logger.debug("Will now execute one-off write task {}, waited in thread pool for {}", task, - millisInThreadPoolWaiting); - executeOperation(task, true, writeOperation); - }, 0L, TimeUnit.MILLISECONDS); - return future; + @Override + public ModbusSlaveEndpoint getEndpoint() { + return endpoint; + } } @Override - public void setEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint, - @Nullable EndpointPoolConfiguration configuration) { - Objects.requireNonNull(connectionFactory, "Not activated!"); - connectionFactory.setEndpointPoolConfiguration(endpoint, configuration); - for (ModbusManagerListener listener : listeners) { - listener.onEndpointPoolConfigurationSet(endpoint, configuration); + public ModbusCommunicationInterface newModbusCommunicationInterface(ModbusSlaveEndpoint endpoint, + @Nullable EndpointPoolConfiguration configuration) throws IllegalArgumentException { + boolean openCommFoundWithSameEndpointDifferentConfig = communicationInterfaces.stream() + .filter(comm -> comm.endpoint.equals(endpoint)) + .anyMatch(comm -> comm.configuration != null && !comm.configuration.equals(configuration)); + if (openCommFoundWithSameEndpointDifferentConfig) { + throw new IllegalArgumentException( + "Communication interface is already open with different configuration to this same endpoint"); } + + ModbusCommunicationInterfaceImpl comm = new ModbusCommunicationInterfaceImpl(endpoint, configuration); + communicationInterfaces.add(comm); + return comm; } @Override @@ -861,19 +895,33 @@ public void setEndpointPoolConfiguration(ModbusSlaveEndpoint endpoint, return connectionFactory.getEndpointPoolConfiguration(endpoint); } - @Override - public void addListener(ModbusManagerListener listener) { - listeners.add(listener); + private void unregisterCommunicationInterface(ModbusCommunicationInterface commInterface) { + communicationInterfaces.remove(commInterface); + maybeCloseConnections(commInterface.getEndpoint()); } - @Override - public void removeListener(ModbusManagerListener listener) { - listeners.remove(listener); - } + private void maybeCloseConnections(ModbusSlaveEndpoint endpoint) { + boolean lastCommWithThisEndpointWasRemoved = communicationInterfaces.stream() + .filter(comm -> comm.endpoint.equals(endpoint)).count() == 0L; + if (lastCommWithThisEndpointWasRemoved) { + // Since last communication interface pointing to this endpoint was closed, we can clean up resources + // and disconnect connections. - @Override - public Set<@NonNull PollTask> getRegisteredRegularPolls() { - return this.scheduledPollTasks.keySet(); + // Make sure connections to this endpoint are closed when they are returned to pool (which + // is usually pretty soon as transactions should be relatively short-lived) + ModbusSlaveConnectionFactoryImpl localConnectionFactory = connectionFactory; + if (localConnectionFactory != null) { + localConnectionFactory.disconnectOnReturn(endpoint, System.currentTimeMillis()); + try { + // Close all idle connections as well (they will be reconnected if necessary on borrow) + if (connectionPool != null) { + connectionPool.clear(endpoint); + } + } catch (Exception e) { + logger.warn("Could not clear endpoint {}. Stack trace follows", endpoint, e); + } + } + } } @Activate @@ -889,7 +937,7 @@ protected void activate(Map configProperties) { .getScheduledPool(MODBUS_POLLER_THREAD_POOL_NAME); } if (scheduledThreadPoolExecutor.isShutdown()) { - logger.error("Thread pool is shut down! Aborting activation of ModbusMangerImpl"); + logger.warn("Thread pool is shut down! Aborting activation of ModbusMangerImpl"); throw new IllegalStateException("Thread pool(s) shut down! Aborting activation of ModbusMangerImpl"); } monitorFuture = scheduledThreadPoolExecutor.scheduleWithFixedDelay(this::logTaskQueueInfo, 0, @@ -902,9 +950,13 @@ protected void deactivate() { synchronized (this) { KeyedObjectPool connectionPool = this.connectionPool; if (connectionPool != null) { - Set<@NonNull PollTask> polls = getRegisteredRegularPolls(); - for (PollTask task : polls) { - unregisterRegularPoll(task); + + for (ModbusCommunicationInterface commInterface : this.communicationInterfaces) { + try { + commInterface.close(); + } catch (Exception e) { + logger.warn("Error when closing communication interface", e); + } } connectionPool.close(); diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusPoolConfig.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusPoolConfig.java index d15c1dc3de0b9..f8d4befde3382 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusPoolConfig.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusPoolConfig.java @@ -18,6 +18,7 @@ import org.apache.commons.pool2.impl.EvictionPolicy; import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.io.transport.modbus.internal.pooling.ModbusSlaveConnectionEvictionPolicy; import net.wimpi.modbus.net.ModbusSlaveConnection; @@ -61,6 +62,10 @@ public ModbusPoolConfig() { // disable JMX setJmxEnabled(false); + + // Evict idle connections every 10 seconds + setEvictionPolicy(new ModbusSlaveConnectionEvictionPolicy()); + setTimeBetweenEvictionRunsMillis(10000); } public EvictionPolicy getEvictionPolicy() { diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusSlaveErrorResponseExceptionImpl.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusSlaveErrorResponseExceptionImpl.java index 70c106ce3d3c7..e8a11bde06c3a 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusSlaveErrorResponseExceptionImpl.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusSlaveErrorResponseExceptionImpl.java @@ -13,7 +13,7 @@ package org.openhab.io.transport.modbus.internal; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.io.transport.modbus.ModbusSlaveErrorResponseException; +import org.openhab.io.transport.modbus.exception.ModbusSlaveErrorResponseException; import net.wimpi.modbus.ModbusSlaveException; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusSlaveIOExceptionImpl.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusSlaveIOExceptionImpl.java index 4d8cf1894f654..27efbd7f461cf 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusSlaveIOExceptionImpl.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/ModbusSlaveIOExceptionImpl.java @@ -15,7 +15,7 @@ import java.io.IOException; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.io.transport.modbus.ModbusSlaveIOException; +import org.openhab.io.transport.modbus.exception.ModbusSlaveIOException; import net.wimpi.modbus.ModbusIOException; diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/RegisterArrayWrappingInputRegister.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/RegisterArrayWrappingInputRegister.java deleted file mode 100644 index 49e30a12f78ce..0000000000000 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/RegisterArrayWrappingInputRegister.java +++ /dev/null @@ -1,88 +0,0 @@ -/** - * 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.io.transport.modbus.internal; - -import java.util.HashMap; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.io.transport.modbus.ModbusRegister; -import org.openhab.io.transport.modbus.ModbusRegisterArray; - -import net.wimpi.modbus.procimg.InputRegister; - -/** - * Implementation of {@link ModbusRegisterArray} which wraps array of {@link InputRegister} - * - * @author Sami Salonen - Initial contribution - */ -@NonNullByDefault -public class RegisterArrayWrappingInputRegister implements ModbusRegisterArray { - - private class RegisterReference implements ModbusRegister { - - private InputRegister wrappedRegister; - - public RegisterReference(int index) { - this.wrappedRegister = wrapped[index]; - } - - @Override - public byte[] getBytes() { - return wrappedRegister.toBytes(); - } - - @Override - public int getValue() { - return wrappedRegister.getValue(); - } - - @Override - public int toUnsignedShort() { - return wrappedRegister.toUnsignedShort(); - } - - @Override - public String toString() { - StringBuffer buffer = new StringBuffer("ModbusRegisterImpl("); - buffer.append("uint16=").append(toUnsignedShort()).append(", hex="); - return appendHexString(buffer).append(')').toString(); - } - } - - private InputRegister[] wrapped; - private Map cache = new HashMap<>(); - - public RegisterArrayWrappingInputRegister(InputRegister[] wrapped) { - this.wrapped = wrapped; - } - - @Override - public ModbusRegister getRegister(int index) { - return cache.computeIfAbsent(index, i -> new RegisterReference(i)); - } - - @Override - public int size() { - return wrapped.length; - } - - @Override - public String toString() { - if (wrapped.length == 0) { - return "RegisterArrayWrappingInputRegister()"; - } - StringBuffer buffer = new StringBuffer(wrapped.length * 2).append("RegisterArrayWrappingInputRegister("); - return appendHexString(buffer).append(')').toString(); - } -} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/pooling/ModbusSlaveConnectionEvictionPolicy.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/pooling/ModbusSlaveConnectionEvictionPolicy.java new file mode 100644 index 0000000000000..beac2802615c7 --- /dev/null +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/pooling/ModbusSlaveConnectionEvictionPolicy.java @@ -0,0 +1,35 @@ +/** + * 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.io.transport.modbus.internal.pooling; + +import org.apache.commons.pool2.PooledObject; +import org.apache.commons.pool2.impl.EvictionConfig; +import org.apache.commons.pool2.impl.EvictionPolicy; +import org.openhab.io.transport.modbus.internal.pooling.ModbusSlaveConnectionFactoryImpl.PooledConnection; + +import net.wimpi.modbus.net.ModbusSlaveConnection; + +/** + * Eviction policy, i.e. policy for deciding when to close idle, unused connections. + * + * Connections are evicted according to {@link PooledConnection} maybeResetConnection method. + * + * @author Sami Salonen - Initial contribution + */ +public class ModbusSlaveConnectionEvictionPolicy implements EvictionPolicy { + + @Override + public boolean evict(EvictionConfig config, PooledObject underTest, int idleCount) { + return ((PooledConnection) underTest).maybeResetConnection("evict"); + } +} diff --git a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/pooling/ModbusSlaveConnectionFactoryImpl.java b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/pooling/ModbusSlaveConnectionFactoryImpl.java index b897ee4da1a8e..dbe6273251cd9 100644 --- a/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/pooling/ModbusSlaveConnectionFactoryImpl.java +++ b/bundles/org.openhab.io.transport.modbus/src/main/java/org/openhab/io/transport/modbus/internal/pooling/ModbusSlaveConnectionFactoryImpl.java @@ -60,9 +60,10 @@ public class ModbusSlaveConnectionFactoryImpl extends BaseKeyedPooledObjectFactory { - private static class PooledConnection extends DefaultPooledObject { + class PooledConnection extends DefaultPooledObject { - private long lastConnected; + private volatile long lastConnected; + private volatile @Nullable ModbusSlaveEndpoint endpoint; public PooledConnection(ModbusSlaveConnection object) { super(object); @@ -72,9 +73,41 @@ public long getLastConnected() { return lastConnected; } - public void setLastConnected(long lastConnected) { + public void setLastConnected(ModbusSlaveEndpoint endpoint, long lastConnected) { + this.endpoint = endpoint; this.lastConnected = lastConnected; } + + public boolean maybeResetConnection(String activityName) { + long localLastConnected = lastConnected; + + ModbusSlaveConnection connection = getObject(); + + @Nullable + EndpointPoolConfiguration configuration = endpointPoolConfigs.get(endpoint); + long reconnectAfterMillis = configuration == null ? 0 : configuration.getReconnectAfterMillis(); + long connectionAgeMillis = System.currentTimeMillis() - localLastConnected; + long disconnectIfConnectedBeforeMillis = disconnectIfConnectedBefore.getOrDefault(endpoint, -1L); + boolean disconnectSinceTooOldConnection = disconnectIfConnectedBeforeMillis < 0L ? false + : localLastConnected <= disconnectIfConnectedBeforeMillis; + boolean shouldBeDisconnected = (reconnectAfterMillis == 0 + || (reconnectAfterMillis > 0 && connectionAgeMillis > reconnectAfterMillis) + || disconnectSinceTooOldConnection); + if (shouldBeDisconnected) { + logger.trace( + "({}) Connection {} (endpoint {}) age {}ms is over the reconnectAfterMillis={}ms limit or has been connection time ({}) is after the \"disconnectBeforeConnectedMillis\"={} -> disconnecting.", + activityName, connection, endpoint, connectionAgeMillis, reconnectAfterMillis, + localLastConnected, disconnectIfConnectedBeforeMillis); + connection.resetConnection(); + return true; + } else { + logger.trace( + "({}) Connection {} (endpoint {}) age ({}ms) is below the reconnectAfterMillis ({}ms) limit and connection time ({}) is after the \"disconnectBeforeConnectedMillis\"={}. Keep the connection open.", + activityName, connection, endpoint, connectionAgeMillis, reconnectAfterMillis, + localLastConnected, disconnectIfConnectedBeforeMillis); + return false; + } + } } private final Logger logger = LoggerFactory.getLogger(ModbusSlaveConnectionFactoryImpl.class); @@ -145,9 +178,6 @@ public void destroyObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject closing the connection", obj.getObject(), endpoint); - if (obj.getObject() == null) { - return; - } obj.getObject().resetConnection(); } @@ -158,9 +188,6 @@ public void activateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject< return; } ModbusSlaveConnection connection = obj.getObject(); - if (connection == null) { - return; - } try { @Nullable EndpointPoolConfiguration config = getEndpointPoolConfiguration(endpoint); @@ -191,38 +218,15 @@ public void passivateObject(ModbusSlaveEndpoint endpoint, @Nullable PooledObject return; } ModbusSlaveConnection connection = obj.getObject(); - if (connection == null) { - return; - } logger.trace("Passivating connection {} for endpoint {}...", connection, endpoint); lastPassivateMillis.put(endpoint, System.currentTimeMillis()); - @Nullable - EndpointPoolConfiguration configuration = endpointPoolConfigs.get(endpoint); - long connected = ((PooledConnection) obj).getLastConnected(); - long reconnectAfterMillis = configuration == null ? 0 : configuration.getReconnectAfterMillis(); - long connectionAgeMillis = System.currentTimeMillis() - ((PooledConnection) obj).getLastConnected(); - long disconnectIfConnectedBeforeMillis = disconnectIfConnectedBefore.getOrDefault(endpoint, -1L); - boolean disconnectSinceTooOldConnection = disconnectIfConnectedBeforeMillis < 0L ? false - : connected <= disconnectIfConnectedBeforeMillis; - if (reconnectAfterMillis == 0 || (reconnectAfterMillis > 0 && connectionAgeMillis > reconnectAfterMillis) - || disconnectSinceTooOldConnection) { - logger.trace( - "(passivate) Connection {} (endpoint {}) age {}ms is over the reconnectAfterMillis={}ms limit or has been connection time ({}) is after the \"disconnectBeforeConnectedMillis\"={} -> disconnecting.", - connection, endpoint, connectionAgeMillis, reconnectAfterMillis, connected, - disconnectIfConnectedBeforeMillis); - connection.resetConnection(); - } else { - logger.trace( - "(passivate) Connection {} (endpoint {}) age ({}ms) is below the reconnectAfterMillis ({}ms) limit and connection time ({}) is after the \"disconnectBeforeConnectedMillis\"={}. Keep the connection open.", - connection, endpoint, connectionAgeMillis, reconnectAfterMillis, connected, - disconnectIfConnectedBeforeMillis); - } + ((PooledConnection) obj).maybeResetConnection("passivate"); logger.trace("...Passivated connection {} for endpoint {}", obj.getObject(), endpoint); } @Override public boolean validateObject(ModbusSlaveEndpoint key, @Nullable PooledObject p) { - boolean valid = p != null && p.getObject() != null && p.getObject().isConnected(); + boolean valid = p != null && p.getObject().isConnected(); logger.trace("Validating endpoint {} connection {} -> {}", key, p.getObject(), valid); return valid; } @@ -291,7 +295,7 @@ private void tryConnect(ModbusSlaveEndpoint endpoint, PooledObject { + return new ModbusRegisterArray(IntStream.of(arr).mapToObj(val -> { ByteBuffer buffer = ByteBuffer.allocate(2); buffer.putShort((short) val); - return new BasicModbusRegister(buffer.get(0), buffer.get(1)); + return new ModbusRegister(buffer.get(0), buffer.get(1)); }).collect(Collectors.toList()).toArray(tmp)); } diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringFromRegistersTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringFromRegistersTest.java index 292cf46912691..ff15788a94099 100644 --- a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringFromRegistersTest.java +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/BitUtilitiesExtractStringFromRegistersTest.java @@ -31,8 +31,6 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; -import org.openhab.io.transport.modbus.BasicModbusRegister; -import org.openhab.io.transport.modbus.BasicModbusRegisterArray; import org.openhab.io.transport.modbus.ModbusBitUtilities; import org.openhab.io.transport.modbus.ModbusRegister; import org.openhab.io.transport.modbus.ModbusRegisterArray; @@ -63,10 +61,10 @@ public BitUtilitiesExtractStringFromRegistersTest(Object expectedResult, ModbusR private static ModbusRegisterArray shortArrayToRegisterArray(int... arr) { ModbusRegister[] tmp = new ModbusRegister[0]; - return new BasicModbusRegisterArray(IntStream.of(arr).mapToObj(val -> { + return new ModbusRegisterArray(IntStream.of(arr).mapToObj(val -> { ByteBuffer buffer = ByteBuffer.allocate(2); buffer.putShort((short) val); - return new BasicModbusRegister(buffer.get(0), buffer.get(1)); + return new ModbusRegister(buffer.get(0), buffer.get(1)); }).collect(Collectors.toList()).toArray(tmp)); } diff --git a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/SmokeTest.java b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/SmokeTest.java index 65f580b52ea10..c0c886d49276f 100644 --- a/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/SmokeTest.java +++ b/bundles/org.openhab.io.transport.modbus/src/test/java/org/openhab/io/transport/modbus/test/SmokeTest.java @@ -14,38 +14,43 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; -import static org.junit.Assume.assumeFalse; - +import static org.junit.Assume.*; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketImpl; +import java.net.SocketImplFactory; +import java.net.UnknownHostException; +import java.util.BitSet; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.commons.lang.StringUtils; +import org.eclipse.jdt.annotation.NonNull; +import org.junit.Before; import org.junit.Test; -import org.openhab.io.transport.modbus.BasicBitArray; -import org.openhab.io.transport.modbus.BasicModbusReadRequestBlueprint; -import org.openhab.io.transport.modbus.BasicModbusWriteCoilRequestBlueprint; -import org.openhab.io.transport.modbus.BasicPollTaskImpl; -import org.openhab.io.transport.modbus.BasicWriteTask; import org.openhab.io.transport.modbus.BitArray; -import org.openhab.io.transport.modbus.ModbusConnectionException; -import org.openhab.io.transport.modbus.ModbusManagerListener; -import org.openhab.io.transport.modbus.ModbusReadCallback; +import org.openhab.io.transport.modbus.ModbusCommunicationInterface; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.ModbusResponse; -import org.openhab.io.transport.modbus.ModbusSlaveErrorResponseException; -import org.openhab.io.transport.modbus.ModbusSlaveIOException; -import org.openhab.io.transport.modbus.ModbusWriteCallback; -import org.openhab.io.transport.modbus.ModbusWriteRequestBlueprint; +import org.openhab.io.transport.modbus.ModbusWriteCoilRequestBlueprint; +import org.openhab.io.transport.modbus.PollTask; import org.openhab.io.transport.modbus.endpoint.EndpointPoolConfiguration; import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; import org.openhab.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint; -import org.openhab.io.transport.modbus.internal.BitArrayWrappingBitVector; +import org.openhab.io.transport.modbus.exception.ModbusConnectionException; +import org.openhab.io.transport.modbus.exception.ModbusSlaveErrorResponseException; +import org.openhab.io.transport.modbus.exception.ModbusSlaveIOException; import org.slf4j.LoggerFactory; import net.wimpi.modbus.msg.ModbusRequest; @@ -54,6 +59,7 @@ import net.wimpi.modbus.procimg.SimpleDigitalIn; import net.wimpi.modbus.procimg.SimpleDigitalOut; import net.wimpi.modbus.procimg.SimpleRegister; +import net.wimpi.modbus.util.BitVector; /** * @author Sami Salonen - Initial contribution @@ -64,6 +70,14 @@ public class SmokeTest extends IntegrationTestSupport { private static final int DISCRETE_EVERY_N_TRUE = 3; private static final int HOLDING_REGISTER_MULTIPLIER = 1; private static final int INPUT_REGISTER_MULTIPLIER = 10; + private static final SpyingSocketFactory socketSpy = new SpyingSocketFactory(); + static { + try { + Socket.setSocketImplFactory(socketSpy); + } catch (IOException e) { + fail("Could not install socket spy in SmokeTest"); + } + } /** * Whether tests are run in Continuous Integration environment, i.e. Jenkins or Travis CI @@ -118,140 +132,113 @@ private void testInputValues(ModbusRegisterArray registers, int offsetInRegister } } + @Before + public void setUpSocketSpy() throws IOException { + socketSpy.sockets.clear(); + } + /** * Test handling of slave error responses. In this case, error code = 2, illegal data address, since no data. * - * @throws InterruptedException + * @throws Exception */ @Test - public void testSlaveReadErrorResponse() throws InterruptedException { + public void testSlaveReadErrorResponse() throws Exception { ModbusSlaveEndpoint endpoint = getEndpoint(); AtomicInteger okCount = new AtomicInteger(); AtomicInteger errorCount = new AtomicInteger(); CountDownLatch callbackCalled = new CountDownLatch(1); AtomicReference lastError = new AtomicReference<>(); - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, + ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), result -> { + assert result.getRegisters().isPresent(); okCount.incrementAndGet(); callbackCalled.countDown(); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { - lastError.set(error); + }, failure -> { errorCount.incrementAndGet(); + lastError.set(failure.getCause()); callbackCalled.countDown(); - } + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { - okCount.incrementAndGet(); - callbackCalled.countDown(); - } - }); - modbusManager.submitOneTimePoll(task); - callbackCalled.await(5, TimeUnit.SECONDS); - assertThat(okCount.get(), is(equalTo(0))); - assertThat(errorCount.get(), is(equalTo(1))); - assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveErrorResponseException); + assertThat(okCount.get(), is(equalTo(0))); + assertThat(errorCount.get(), is(equalTo(1))); + assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveErrorResponseException); + } } /** * Test handling of connection error responses. * - * @throws InterruptedException + * @throws Exception */ @Test - public void testSlaveConnectionError() throws InterruptedException { + public void testSlaveConnectionError() throws Exception { // In the test we have non-responding slave (see http://stackoverflow.com/a/904609), and we use short connection // timeout ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("10.255.255.1", 9999); EndpointPoolConfiguration configuration = new EndpointPoolConfiguration(); configuration.setConnectTimeoutMillis(100); - modbusManager.setEndpointPoolConfiguration(endpoint, configuration); AtomicInteger okCount = new AtomicInteger(); AtomicInteger errorCount = new AtomicInteger(); CountDownLatch callbackCalled = new CountDownLatch(1); AtomicReference lastError = new AtomicReference<>(); - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, + configuration)) { + comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, + ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), result -> { + assert result.getRegisters().isPresent(); okCount.incrementAndGet(); callbackCalled.countDown(); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { - lastError.set(error); + }, failure -> { errorCount.incrementAndGet(); + lastError.set(failure.getCause()); callbackCalled.countDown(); - } + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { - okCount.incrementAndGet(); - callbackCalled.countDown(); - } - }); - modbusManager.submitOneTimePoll(task); - callbackCalled.await(5, TimeUnit.SECONDS); - assertThat(okCount.get(), is(equalTo(0))); - assertThat(errorCount.get(), is(equalTo(1))); - assertTrue(lastError.toString(), lastError.get() instanceof ModbusConnectionException); + assertThat(okCount.get(), is(equalTo(0))); + assertThat(errorCount.get(), is(equalTo(1))); + assertTrue(lastError.toString(), lastError.get() instanceof ModbusConnectionException); + } } /** * Have super slow connection response, eventually resulting as timeout (due to default timeout of 3 s in * net.wimpi.modbus.Modbus.DEFAULT_TIMEOUT) * - * @throws InterruptedException + * @throws Exception */ @Test - public void testIOError() throws InterruptedException { - artificialServerWait = 30000; + public void testIOError() throws Exception { + artificialServerWait = 60000; ModbusSlaveEndpoint endpoint = getEndpoint(); AtomicInteger okCount = new AtomicInteger(); AtomicInteger errorCount = new AtomicInteger(); CountDownLatch callbackCalled = new CountDownLatch(1); AtomicReference lastError = new AtomicReference<>(); - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, + ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 0, 5, 1), result -> { + assert result.getRegisters().isPresent(); okCount.incrementAndGet(); callbackCalled.countDown(); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { - lastError.set(error); + }, failure -> { errorCount.incrementAndGet(); + lastError.set(failure.getCause()); callbackCalled.countDown(); - } - - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { - okCount.incrementAndGet(); - callbackCalled.countDown(); - } - }); - modbusManager.submitOneTimePoll(task); - callbackCalled.await(15, TimeUnit.SECONDS); - assertThat(okCount.get(), is(equalTo(0))); - assertThat(lastError.toString(), errorCount.get(), is(equalTo(1))); - assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveIOException); + }); + assertTrue(callbackCalled.await(15, TimeUnit.SECONDS)); + assertThat(okCount.get(), is(equalTo(0))); + assertThat(lastError.toString(), errorCount.get(), is(equalTo(1))); + assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveIOException); + } } - public void testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode functionCode, int count) - throws InterruptedException { + public void testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode functionCode, int count) throws Exception { assertThat(functionCode, is(anyOf(equalTo(ModbusReadFunctionCode.READ_INPUT_DISCRETES), equalTo(ModbusReadFunctionCode.READ_COILS)))); generateData(); @@ -263,104 +250,98 @@ public void testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode functionCod final int offset = 1; - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, - new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, functionCode, offset, count, 1), - new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - unexpectedCount.incrementAndGet(); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, functionCode, offset, count, 1), + result -> { + Optional<@NonNull BitArray> bitsOptional = result.getBits(); + if (bitsOptional.isPresent()) { + lastData.set(bitsOptional.get()); + } else { + unexpectedCount.incrementAndGet(); + } callbackCalled.countDown(); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { + }, failure -> { unexpectedCount.incrementAndGet(); callbackCalled.countDown(); - } + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { - lastData.set(bits); - callbackCalled.countDown(); - } - }); - modbusManager.submitOneTimePoll(task); - callbackCalled.await(5, TimeUnit.SECONDS); - assertThat(unexpectedCount.get(), is(equalTo(0))); - BitArray bits = (BitArray) lastData.get(); - assertThat(bits.size(), is(equalTo(count))); - if (functionCode == ModbusReadFunctionCode.READ_INPUT_DISCRETES) { - testDiscreteValues(bits, offset); - } else { - testCoilValues(bits, offset); + assertThat(unexpectedCount.get(), is(equalTo(0))); + BitArray bits = (BitArray) lastData.get(); + assertThat(bits, notNullValue()); + assertThat(bits.size(), is(equalTo(count))); + if (functionCode == ModbusReadFunctionCode.READ_INPUT_DISCRETES) { + testDiscreteValues(bits, offset); + } else { + testCoilValues(bits, offset); + } } } @Test - public void testOneOffReadWithDiscrete1() throws InterruptedException { + public void testOneOffReadWithDiscrete1() throws Exception { testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 1); } @Test - public void testOneOffReadWithDiscrete7() throws InterruptedException { + public void testOneOffReadWithDiscrete7() throws Exception { // less than byte testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 7); } @Test - public void testOneOffReadWithDiscrete8() throws InterruptedException { + public void testOneOffReadWithDiscrete8() throws Exception { // exactly one byte testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 8); } @Test - public void testOneOffReadWithDiscrete13() throws InterruptedException { + public void testOneOffReadWithDiscrete13() throws Exception { // larger than byte, less than word (16 bit) testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 13); } @Test - public void testOneOffReadWithDiscrete18() throws InterruptedException { + public void testOneOffReadWithDiscrete18() throws Exception { // larger than word (16 bit) testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_INPUT_DISCRETES, 18); } @Test - public void testOneOffReadWithCoils1() throws InterruptedException { + public void testOneOffReadWithCoils1() throws Exception { testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 1); } @Test - public void testOneOffReadWithCoils7() throws InterruptedException { + public void testOneOffReadWithCoils7() throws Exception { // less than byte testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 7); } @Test - public void testOneOffReadWithCoils8() throws InterruptedException { + public void testOneOffReadWithCoils8() throws Exception { // exactly one byte testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 8); } @Test - public void testOneOffReadWithCoils13() throws InterruptedException { + public void testOneOffReadWithCoils13() throws Exception { // larger than byte, less than word (16 bit) testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 13); } @Test - public void testOneOffReadWithCoils18() throws InterruptedException { + public void testOneOffReadWithCoils18() throws Exception { // larger than word (16 bit) testOneOffReadWithDiscreteOrCoils(ModbusReadFunctionCode.READ_COILS, 18); } /** * - * @throws InterruptedException + * @throws Exception */ @Test - public void testOneOffReadWithHolding() throws InterruptedException { + public void testOneOffReadWithHolding() throws Exception { generateData(); ModbusSlaveEndpoint endpoint = getEndpoint(); @@ -368,84 +349,70 @@ public void testOneOffReadWithHolding() throws InterruptedException { CountDownLatch callbackCalled = new CountDownLatch(1); AtomicReference lastData = new AtomicReference<>(); - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - lastData.set(registers); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, + ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), result -> { + Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters(); + if (registersOptional.isPresent()) { + lastData.set(registersOptional.get()); + } else { + unexpectedCount.incrementAndGet(); + } callbackCalled.countDown(); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { + }, failure -> { unexpectedCount.incrementAndGet(); callbackCalled.countDown(); - } - - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { - unexpectedCount.incrementAndGet(); + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); - callbackCalled.countDown(); - } - }); - modbusManager.submitOneTimePoll(task); - callbackCalled.await(5, TimeUnit.SECONDS); - assertThat(unexpectedCount.get(), is(equalTo(0))); - ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get(); - assertThat(registers.size(), is(equalTo(15))); - testHoldingValues(registers, 1); + assertThat(unexpectedCount.get(), is(equalTo(0))); + ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get(); + assertThat(registers.size(), is(equalTo(15))); + testHoldingValues(registers, 1); + } } /** * - * @throws InterruptedException + * @throws Exception */ @Test - public void testOneOffReadWithInput() throws InterruptedException { + public void testOneOffReadWithInput() throws Exception { generateData(); ModbusSlaveEndpoint endpoint = getEndpoint(); AtomicInteger unexpectedCount = new AtomicInteger(); CountDownLatch callbackCalled = new CountDownLatch(1); AtomicReference lastData = new AtomicReference<>(); - - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_INPUT_REGISTERS, 1, 15, 1), new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - lastData.set(registers); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.submitOneTimePoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, + ModbusReadFunctionCode.READ_INPUT_REGISTERS, 1, 15, 1), result -> { + Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters(); + if (registersOptional.isPresent()) { + lastData.set(registersOptional.get()); + } else { + unexpectedCount.incrementAndGet(); + } callbackCalled.countDown(); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { + }, failure -> { unexpectedCount.incrementAndGet(); callbackCalled.countDown(); - } + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { - unexpectedCount.incrementAndGet(); - callbackCalled.countDown(); - } - }); - modbusManager.submitOneTimePoll(task); - callbackCalled.await(5, TimeUnit.SECONDS); - assertThat(unexpectedCount.get(), is(equalTo(0))); - ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get(); - assertThat(registers.size(), is(equalTo(15))); - testInputValues(registers, 1); + assertThat(unexpectedCount.get(), is(equalTo(0))); + ModbusRegisterArray registers = (ModbusRegisterArray) lastData.get(); + assertThat(registers.size(), is(equalTo(15))); + testInputValues(registers, 1); + } } /** * - * @throws InterruptedException + * @throws Exception */ @Test - public void testOneOffWriteMultipleCoil() throws InterruptedException { + public void testOneOffWriteMultipleCoil() throws Exception { LoggerFactory.getLogger(this.getClass()).error("STARTING MULTIPLE"); generateData(); ModbusSlaveEndpoint endpoint = getEndpoint(); @@ -453,46 +420,40 @@ public void testOneOffWriteMultipleCoil() throws InterruptedException { AtomicInteger unexpectedCount = new AtomicInteger(); AtomicReference lastData = new AtomicReference<>(); - BitArray bits = new BasicBitArray(true, true, false, false, true, true); - BasicWriteTask task = new BasicWriteTask(endpoint, - new BasicModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, true, 1), new ModbusWriteCallback() { - - @Override - public void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response) { - lastData.set(response); - } - - @Override - public void onError(ModbusWriteRequestBlueprint request, Exception error) { - unexpectedCount.incrementAndGet(); - } - }); - modbusManager.submitOneTimeWrite(task); - waitForAssert(() -> { - assertThat(unexpectedCount.get(), is(equalTo(0))); - assertThat(lastData.get(), is(notNullValue())); - - ModbusResponse response = (ModbusResponse) lastData.get(); - assertThat(response.getFunctionCode(), is(equalTo(15))); - - assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1))); - ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0); - assertThat(request.getFunctionCode(), is(equalTo(15))); - assertThat(((WriteMultipleCoilsRequest) request).getReference(), is(equalTo(3))); - assertThat(((WriteMultipleCoilsRequest) request).getBitCount(), is(equalTo(bits.size()))); - assertThat(new BitArrayWrappingBitVector(((WriteMultipleCoilsRequest) request).getCoils(), bits.size()), - is(equalTo(bits))); - }, 6000, 10); + BitArray bits = new BitArray(true, true, false, false, true, true); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.submitOneTimeWrite(new ModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, true, 1), result -> { + lastData.set(result.getResponse()); + }, failure -> { + unexpectedCount.incrementAndGet(); + }); + waitForAssert(() -> { + assertThat(unexpectedCount.get(), is(equalTo(0))); + assertThat(lastData.get(), is(notNullValue())); + + ModbusResponse response = (ModbusResponse) lastData.get(); + assertThat(response.getFunctionCode(), is(equalTo(15))); + + assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1))); + ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0); + assertThat(request.getFunctionCode(), is(equalTo(15))); + assertThat(((WriteMultipleCoilsRequest) request).getReference(), is(equalTo(3))); + assertThat(((WriteMultipleCoilsRequest) request).getBitCount(), is(equalTo(bits.size()))); + BitVector writeRequestCoils = ((WriteMultipleCoilsRequest) request).getCoils(); + BitArray writtenBits = new BitArray(BitSet.valueOf(writeRequestCoils.getBytes()), bits.size()); + assertThat(writtenBits, is(equalTo(bits))); + }, 6000, 10); + } LoggerFactory.getLogger(this.getClass()).error("ENDINGMULTIPLE"); } /** * Write is out-of-bounds, slave should return error * - * @throws InterruptedException + * @throws Exception */ @Test - public void testOneOffWriteMultipleCoilError() throws InterruptedException { + public void testOneOffWriteMultipleCoilError() throws Exception { generateData(); ModbusSlaveEndpoint endpoint = getEndpoint(); @@ -500,43 +461,37 @@ public void testOneOffWriteMultipleCoilError() throws InterruptedException { CountDownLatch callbackCalled = new CountDownLatch(1); AtomicReference lastError = new AtomicReference<>(); - BitArray bits = new BasicBitArray(500); - BasicWriteTask task = new BasicWriteTask(endpoint, - new BasicModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, true, 1), new ModbusWriteCallback() { - - @Override - public void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response) { - unexpectedCount.incrementAndGet(); - callbackCalled.countDown(); - } - - @Override - public void onError(ModbusWriteRequestBlueprint request, Exception error) { - lastError.set(error); - callbackCalled.countDown(); - } - }); - modbusManager.submitOneTimeWrite(task); - callbackCalled.await(5, TimeUnit.SECONDS); + BitArray bits = new BitArray(500); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.submitOneTimeWrite(new ModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, true, 1), result -> { + unexpectedCount.incrementAndGet(); + callbackCalled.countDown(); + }, failure -> { + lastError.set(failure.getCause()); + callbackCalled.countDown(); + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); - assertThat(unexpectedCount.get(), is(equalTo(0))); - assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveErrorResponseException); + assertThat(unexpectedCount.get(), is(equalTo(0))); + assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveErrorResponseException); - assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1))); - ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0); - assertThat(request.getFunctionCode(), is(equalTo(15))); - assertThat(((WriteMultipleCoilsRequest) request).getReference(), is(equalTo(3))); - assertThat(((WriteMultipleCoilsRequest) request).getBitCount(), is(equalTo(bits.size()))); - assertThat(new BitArrayWrappingBitVector(((WriteMultipleCoilsRequest) request).getCoils(), bits.size()), - is(equalTo(bits))); + assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1))); + ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0); + assertThat(request.getFunctionCode(), is(equalTo(15))); + assertThat(((WriteMultipleCoilsRequest) request).getReference(), is(equalTo(3))); + assertThat(((WriteMultipleCoilsRequest) request).getBitCount(), is(equalTo(bits.size()))); + BitVector writeRequestCoils = ((WriteMultipleCoilsRequest) request).getCoils(); + BitArray writtenBits = new BitArray(BitSet.valueOf(writeRequestCoils.getBytes()), bits.size()); + assertThat(writtenBits, is(equalTo(bits))); + } } /** * - * @throws InterruptedException + * @throws Exception */ @Test - public void testOneOffWriteSingleCoil() throws InterruptedException { + public void testOneOffWriteSingleCoil() throws Exception { generateData(); ModbusSlaveEndpoint endpoint = getEndpoint(); @@ -544,44 +499,37 @@ public void testOneOffWriteSingleCoil() throws InterruptedException { CountDownLatch callbackCalled = new CountDownLatch(1); AtomicReference lastData = new AtomicReference<>(); - BitArray bits = new BasicBitArray(true); - BasicWriteTask task = new BasicWriteTask(endpoint, - new BasicModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, false, 1), new ModbusWriteCallback() { - - @Override - public void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response) { - lastData.set(response); - callbackCalled.countDown(); - } - - @Override - public void onError(ModbusWriteRequestBlueprint request, Exception error) { - unexpectedCount.incrementAndGet(); - callbackCalled.countDown(); - } - }); - modbusManager.submitOneTimeWrite(task); - callbackCalled.await(5, TimeUnit.SECONDS); + BitArray bits = new BitArray(true); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.submitOneTimeWrite(new ModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 3, bits, false, 1), result -> { + lastData.set(result.getResponse()); + callbackCalled.countDown(); + }, failure -> { + unexpectedCount.incrementAndGet(); + callbackCalled.countDown(); + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); - assertThat(unexpectedCount.get(), is(equalTo(0))); - ModbusResponse response = (ModbusResponse) lastData.get(); - assertThat(response.getFunctionCode(), is(equalTo(5))); + assertThat(unexpectedCount.get(), is(equalTo(0))); + ModbusResponse response = (ModbusResponse) lastData.get(); + assertThat(response.getFunctionCode(), is(equalTo(5))); - assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1))); - ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0); - assertThat(request.getFunctionCode(), is(equalTo(5))); - assertThat(((WriteCoilRequest) request).getReference(), is(equalTo(3))); - assertThat(((WriteCoilRequest) request).getCoil(), is(equalTo(true))); + assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1))); + ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0); + assertThat(request.getFunctionCode(), is(equalTo(5))); + assertThat(((WriteCoilRequest) request).getReference(), is(equalTo(3))); + assertThat(((WriteCoilRequest) request).getCoil(), is(equalTo(true))); + } } /** * * Write is out-of-bounds, slave should return error * - * @throws InterruptedException + * @throws Exception */ @Test - public void testOneOffWriteSingleCoilError() throws InterruptedException { + public void testOneOffWriteSingleCoilError() throws Exception { generateData(); ModbusSlaveEndpoint endpoint = getEndpoint(); @@ -589,34 +537,27 @@ public void testOneOffWriteSingleCoilError() throws InterruptedException { CountDownLatch callbackCalled = new CountDownLatch(1); AtomicReference lastError = new AtomicReference<>(); - BitArray bits = new BasicBitArray(true); - BasicWriteTask task = new BasicWriteTask(endpoint, - new BasicModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 300, bits, false, 1), - new ModbusWriteCallback() { - - @Override - public void onWriteResponse(ModbusWriteRequestBlueprint request, ModbusResponse response) { + BitArray bits = new BitArray(true); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.submitOneTimeWrite(new ModbusWriteCoilRequestBlueprint(SLAVE_UNIT_ID, 300, bits, false, 1), + result -> { unexpectedCount.incrementAndGet(); callbackCalled.countDown(); - } - - @Override - public void onError(ModbusWriteRequestBlueprint request, Exception error) { - lastError.set(error); + }, failure -> { + lastError.set(failure.getCause()); callbackCalled.countDown(); - } - }); - modbusManager.submitOneTimeWrite(task); - callbackCalled.await(5, TimeUnit.SECONDS); + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); - assertThat(unexpectedCount.get(), is(equalTo(0))); - assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveErrorResponseException); + assertThat(unexpectedCount.get(), is(equalTo(0))); + assertTrue(lastError.toString(), lastError.get() instanceof ModbusSlaveErrorResponseException); - assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1))); - ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0); - assertThat(request.getFunctionCode(), is(equalTo(5))); - assertThat(((WriteCoilRequest) request).getReference(), is(equalTo(300))); - assertThat(((WriteCoilRequest) request).getCoil(), is(equalTo(true))); + assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(1))); + ModbusRequest request = modbustRequestCaptor.getAllReturnValues().get(0); + assertThat(request.getFunctionCode(), is(equalTo(5))); + assertThat(((WriteCoilRequest) request).getReference(), is(equalTo(300))); + assertThat(((WriteCoilRequest) request).getCoil(), is(equalTo(true))); + } } /** @@ -624,10 +565,10 @@ public void onError(ModbusWriteRequestBlueprint request, Exception error) { * * Amount of requests is timed, and average poll period is checked * - * @throws InterruptedException + * @throws Exception */ @Test - public void testRegularReadEvery150msWithCoil() throws InterruptedException { + public void testRegularReadEvery150msWithCoil() throws Exception { generateData(); ModbusSlaveEndpoint endpoint = getEndpoint(); @@ -635,41 +576,34 @@ public void testRegularReadEvery150msWithCoil() throws InterruptedException { CountDownLatch callbackCalled = new CountDownLatch(5); AtomicInteger dataReceived = new AtomicInteger(); - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, - new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, ModbusReadFunctionCode.READ_COILS, 1, 15, 1), - new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - unexpectedCount.incrementAndGet(); + long start = System.currentTimeMillis(); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.registerRegularPoll( + new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, ModbusReadFunctionCode.READ_COILS, 1, 15, 1), 150, 0, + result -> { + Optional<@NonNull BitArray> bitsOptional = result.getBits(); + if (bitsOptional.isPresent()) { + BitArray bits = bitsOptional.get(); + dataReceived.incrementAndGet(); + try { + assertThat(bits.size(), is(equalTo(15))); + testCoilValues(bits, 1); + } catch (AssertionError e) { + unexpectedCount.incrementAndGet(); + } + } else { + unexpectedCount.incrementAndGet(); + } callbackCalled.countDown(); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { + }, failure -> { unexpectedCount.incrementAndGet(); callbackCalled.countDown(); - } + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { - dataReceived.incrementAndGet(); - - try { - assertThat(bits.size(), is(equalTo(15))); - testCoilValues(bits, 1); - } catch (AssertionError e) { - unexpectedCount.incrementAndGet(); - } - - callbackCalled.countDown(); - } - }); - long start = System.currentTimeMillis(); - modbusManager.registerRegularPoll(task, 150, 0); - callbackCalled.await(5, TimeUnit.SECONDS); - long end = System.currentTimeMillis(); - assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500); + long end = System.currentTimeMillis(); + assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500); + } } /** @@ -677,10 +611,10 @@ public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { * * Amount of requests is timed, and average poll period is checked * - * @throws InterruptedException + * @throws Exception */ @Test - public void testRegularReadEvery150msWithHolding() throws InterruptedException { + public void testRegularReadEvery150msWithHolding() throws Exception { generateData(); ModbusSlaveEndpoint endpoint = getEndpoint(); @@ -688,44 +622,36 @@ public void testRegularReadEvery150msWithHolding() throws InterruptedException { CountDownLatch callbackCalled = new CountDownLatch(5); AtomicInteger dataReceived = new AtomicInteger(); - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - dataReceived.incrementAndGet(); - - try { - assertThat(registers.size(), is(equalTo(15))); - testHoldingValues(registers, 1); - } catch (AssertionError e) { + long start = System.currentTimeMillis(); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.registerRegularPoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, + ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), 150, 0, result -> { + Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters(); + if (registersOptional.isPresent()) { + ModbusRegisterArray registers = registersOptional.get(); + dataReceived.incrementAndGet(); + try { + assertThat(registers.size(), is(equalTo(15))); + testHoldingValues(registers, 1); + } catch (AssertionError e) { + unexpectedCount.incrementAndGet(); + } + } else { unexpectedCount.incrementAndGet(); } - - callbackCalled.countDown(); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { - unexpectedCount.incrementAndGet(); callbackCalled.countDown(); - } - - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { + }, failure -> { unexpectedCount.incrementAndGet(); callbackCalled.countDown(); - } - }); - long start = System.currentTimeMillis(); - modbusManager.registerRegularPoll(task, 150, 0); - callbackCalled.await(5, TimeUnit.SECONDS); - long end = System.currentTimeMillis(); - assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500); + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); + long end = System.currentTimeMillis(); + assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500); + } } @Test - public void testRegularReadFirstErrorThenOK() throws InterruptedException { + public void testRegularReadFirstErrorThenOK() throws Exception { generateData(); ModbusSlaveEndpoint endpoint = getEndpoint(); @@ -733,41 +659,33 @@ public void testRegularReadFirstErrorThenOK() throws InterruptedException { CountDownLatch callbackCalled = new CountDownLatch(5); AtomicInteger dataReceived = new AtomicInteger(); - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), new ModbusReadCallback() { - - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - dataReceived.incrementAndGet(); + long start = System.currentTimeMillis(); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.registerRegularPoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, + ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), 150, 0, result -> { + Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters(); + if (registersOptional.isPresent()) { + ModbusRegisterArray registers = registersOptional.get(); + dataReceived.incrementAndGet(); + try { + assertThat(registers.size(), is(equalTo(15))); + testHoldingValues(registers, 1); + } catch (AssertionError e) { + unexpectedCount.incrementAndGet(); + } - try { - assertThat(registers.size(), is(equalTo(15))); - testHoldingValues(registers, 1); - } catch (AssertionError e) { + } else { unexpectedCount.incrementAndGet(); } - callbackCalled.countDown(); - } - - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { - unexpectedCount.incrementAndGet(); - callbackCalled.countDown(); - } - - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { + }, failure -> { unexpectedCount.incrementAndGet(); callbackCalled.countDown(); - } - }); - long start = System.currentTimeMillis(); - modbusManager.registerRegularPoll(task, 150, 0); - callbackCalled.await(5, TimeUnit.SECONDS); - modbusManager.unregisterRegularPoll(task); - long end = System.currentTimeMillis(); - assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500); + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); + long end = System.currentTimeMillis(); + assertPollDetails(unexpectedCount, dataReceived, start, end, 145, 500); + } } /** @@ -797,25 +715,71 @@ private void assertPollDetails(AtomicInteger unexpectedCount, AtomicInteger expe } @Test - public void testUnregisterPolling() throws InterruptedException { + public void testUnregisterPollingOnClose() throws Exception { ModbusSlaveEndpoint endpoint = getEndpoint(); AtomicInteger unexpectedCount = new AtomicInteger(); AtomicInteger errorCount = new AtomicInteger(); - CountDownLatch callbackCalled = new CountDownLatch(3); + CountDownLatch successfulCountDownLatch = new CountDownLatch(3); AtomicInteger expectedReceived = new AtomicInteger(); - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), new ModbusReadCallback() { + long start = System.currentTimeMillis(); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + comms.registerRegularPoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, + ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), 200, 0, result -> { + Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters(); + if (registersOptional.isPresent()) { + expectedReceived.incrementAndGet(); + successfulCountDownLatch.countDown(); + } else { + // bits + unexpectedCount.incrementAndGet(); + } + }, failure -> { + if (spi.getDigitalInCount() > 0) { + // No errors expected after server filled with data + unexpectedCount.incrementAndGet(); + } else { + expectedReceived.incrementAndGet(); + errorCount.incrementAndGet(); + generateData(); + successfulCountDownLatch.countDown(); + } + }); + // Wait for N successful responses before proceeding with assertions of poll rate + assertTrue(successfulCountDownLatch.await(60, TimeUnit.SECONDS)); - @Override - public void onRegisters(ModbusReadRequestBlueprint request, ModbusRegisterArray registers) { - expectedReceived.incrementAndGet(); - callbackCalled.countDown(); - } + long end = System.currentTimeMillis(); + assertPollDetails(unexpectedCount, expectedReceived, start, end, 190, 600); - @Override - public void onError(ModbusReadRequestBlueprint request, Exception error) { + // wait some more and ensure nothing comes back + Thread.sleep(500); + assertThat(unexpectedCount.get(), is(equalTo(0))); + } + } + + @Test + public void testUnregisterPollingExplicit() throws Exception { + ModbusSlaveEndpoint endpoint = getEndpoint(); + + AtomicInteger unexpectedCount = new AtomicInteger(); + AtomicInteger errorCount = new AtomicInteger(); + CountDownLatch callbackCalled = new CountDownLatch(3); + AtomicInteger expectedReceived = new AtomicInteger(); + + long start = System.currentTimeMillis(); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, null)) { + PollTask task = comms.registerRegularPoll(new ModbusReadRequestBlueprint(SLAVE_UNIT_ID, + ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), 200, 0, result -> { + Optional<@NonNull ModbusRegisterArray> registersOptional = result.getRegisters(); + if (registersOptional.isPresent()) { + expectedReceived.incrementAndGet(); + } else { + // bits + unexpectedCount.incrementAndGet(); + } + callbackCalled.countDown(); + }, failure -> { if (spi.getDigitalInCount() > 0) { // No errors expected after server filled with data unexpectedCount.incrementAndGet(); @@ -823,103 +787,209 @@ public void onError(ModbusReadRequestBlueprint request, Exception error) { expectedReceived.incrementAndGet(); errorCount.incrementAndGet(); generateData(); - callbackCalled.countDown(); } - } + }); + assertTrue(callbackCalled.await(60, TimeUnit.SECONDS)); + long end = System.currentTimeMillis(); + assertPollDetails(unexpectedCount, expectedReceived, start, end, 190, 600); - @Override - public void onBits(ModbusReadRequestBlueprint request, BitArray bits) { - unexpectedCount.incrementAndGet(); - callbackCalled.countDown(); - } - }); - long start = System.currentTimeMillis(); - modbusManager.registerRegularPoll(task, 200, 0); - callbackCalled.await(5, TimeUnit.SECONDS); - modbusManager.unregisterRegularPoll(task); - long end = System.currentTimeMillis(); - assertPollDetails(unexpectedCount, expectedReceived, start, end, 190, 600); - - // wait some more and ensure nothing comes back - Thread.sleep(500); - assertThat(unexpectedCount.get(), is(equalTo(0))); + // Explicitly unregister the regular poll + comms.unregisterRegularPoll(task); + + // wait some more and ensure nothing comes back + Thread.sleep(500); + assertThat(unexpectedCount.get(), is(equalTo(0))); + } } @SuppressWarnings("null") @Test - public void testPoolConfigurationWithoutListener() { + public void testPoolConfigurationWithoutListener() throws Exception { EndpointPoolConfiguration defaultConfig = modbusManager.getEndpointPoolConfiguration(getEndpoint()); assertThat(defaultConfig, is(notNullValue())); EndpointPoolConfiguration newConfig = new EndpointPoolConfiguration(); newConfig.setConnectMaxTries(5); - modbusManager.setEndpointPoolConfiguration(getEndpoint(), newConfig); + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(getEndpoint(), + newConfig)) { + // Sets configuration for the endpoint implicitly + } + assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()).getConnectMaxTries(), is(equalTo(5))); assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(not(equalTo(defaultConfig)))); // Reset config - modbusManager.setEndpointPoolConfiguration(getEndpoint(), null); - // Should matc hdefault + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(getEndpoint(), null)) { + // Sets configuration for the endpoint implicitly + } + // Should match the default assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(equalTo(defaultConfig))); } - @SuppressWarnings("null") @Test - public void testPoolConfigurationListenerAndChanges() { - AtomicInteger expectedCount = new AtomicInteger(); - AtomicInteger unexpectedCount = new AtomicInteger(); - CountDownLatch callbackCalled = new CountDownLatch(2); - modbusManager.addListener(new ModbusManagerListener() { - - @Override - public void onEndpointPoolConfigurationSet(ModbusSlaveEndpoint endpoint, - EndpointPoolConfiguration configuration) { - if ((callbackCalled.getCount() == 2L && configuration.getConnectMaxTries() == 50) - || (callbackCalled.getCount() == 1L && configuration == null)) { - expectedCount.incrementAndGet(); - } else { - unexpectedCount.incrementAndGet(); + public void testConnectionCloseAfterLastCommunicationInterfaceClosed() throws IllegalArgumentException, Exception { + assumeFalse("Running in CI! Will not test timing-sensitive details", isRunningInCI()); + ModbusSlaveEndpoint endpoint = getEndpoint(); + assumeTrue("Connection closing test supported only with TCP slaves", + endpoint instanceof ModbusTCPSlaveEndpoint); + + // Generate server data + generateData(); + + EndpointPoolConfiguration config = new EndpointPoolConfiguration(); + config.setReconnectAfterMillis(9_000_000); + + // 1. capture open connections at this point + long openSocketsBefore = getNumberOfOpenClients(socketSpy); + assertThat(openSocketsBefore, is(equalTo(0L))); + + // 2. make poll, binding opens the tcp connection + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, config)) { + { + CountDownLatch latch = new CountDownLatch(1); + comms.submitOneTimePoll(new ModbusReadRequestBlueprint(1, ModbusReadFunctionCode.READ_COILS, 0, 1, 1), + response -> { + latch.countDown(); + }, failure -> { + latch.countDown(); + }); + assertTrue(latch.await(60, TimeUnit.SECONDS)); + } + waitForAssert(() -> { + // 3. ensure one open connection + long openSocketsAfter = getNumberOfOpenClients(socketSpy); + assertThat(openSocketsAfter, is(equalTo(1L))); + }); + try (ModbusCommunicationInterface comms2 = modbusManager.newModbusCommunicationInterface(endpoint, + config)) { + { + CountDownLatch latch = new CountDownLatch(1); + comms.submitOneTimePoll( + new ModbusReadRequestBlueprint(1, ModbusReadFunctionCode.READ_COILS, 0, 1, 1), response -> { + latch.countDown(); + }, failure -> { + latch.countDown(); + }); + assertTrue(latch.await(60, TimeUnit.SECONDS)); } - callbackCalled.countDown(); + assertThat(getNumberOfOpenClients(socketSpy), is(equalTo(1L))); + // wait for moment (to check that no connections are closed) + Thread.sleep(1000); + // no more than 1 connection, even though requests are going through + assertThat(getNumberOfOpenClients(socketSpy), is(equalTo(1L))); } + Thread.sleep(1000); + // Still one connection open even after closing second connection + assertThat(getNumberOfOpenClients(socketSpy), is(equalTo(1L))); + + } // 4. close (the last) comms + // ensure that open connections are closed + // (despite huge "reconnect after millis") + waitForAssert(() -> { + long openSocketsAfterClose = getNumberOfOpenClients(socketSpy); + assertThat(openSocketsAfterClose, is(equalTo(0L))); }); - EndpointPoolConfiguration defaultConfig = modbusManager.getEndpointPoolConfiguration(getEndpoint()); - assertThat(defaultConfig, is(notNullValue())); + } - EndpointPoolConfiguration newConfig = new EndpointPoolConfiguration(); - newConfig.setConnectMaxTries(50); - modbusManager.setEndpointPoolConfiguration(getEndpoint(), newConfig); - assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()).getConnectMaxTries(), is(equalTo(50))); - assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(not(equalTo(defaultConfig)))); + @Test + public void testConnectionCloseAfterOneOffPoll() throws IllegalArgumentException, Exception { + assumeFalse("Running in CI! Will not test timing-sensitive details", isRunningInCI()); + ModbusSlaveEndpoint endpoint = getEndpoint(); + assumeTrue("Connection closing test supported only with TCP slaves", + endpoint instanceof ModbusTCPSlaveEndpoint); - assertThat(unexpectedCount.get(), is(equalTo(0))); - assertThat(callbackCalled.getCount(), is(equalTo(1L))); + // Generate server data + generateData(); - // Reset config - modbusManager.setEndpointPoolConfiguration(getEndpoint(), null); - // Should match default - assertThat(modbusManager.getEndpointPoolConfiguration(getEndpoint()), is(equalTo(defaultConfig))); + EndpointPoolConfiguration config = new EndpointPoolConfiguration(); + config.setReconnectAfterMillis(2_000); + + // 1. capture open connections at this point + long openSocketsBefore = getNumberOfOpenClients(socketSpy); + assertThat(openSocketsBefore, is(equalTo(0L))); + + // 2. make poll, binding opens the tcp connection + try (ModbusCommunicationInterface comms = modbusManager.newModbusCommunicationInterface(endpoint, config)) { + { + CountDownLatch latch = new CountDownLatch(1); + comms.submitOneTimePoll(new ModbusReadRequestBlueprint(1, ModbusReadFunctionCode.READ_COILS, 0, 1, 1), + response -> { + latch.countDown(); + }, failure -> { + latch.countDown(); + }); + assertTrue(latch.await(60, TimeUnit.SECONDS)); + } + // Right after the poll we should have one connection open + waitForAssert(() -> { + // 3. ensure one open connection + long openSocketsAfter = getNumberOfOpenClients(socketSpy); + assertThat(openSocketsAfter, is(equalTo(1L))); + }); + // 4. Connection should close itself by the commons pool eviction policy (checking for old idle connection + // every now and then) + waitForAssert(() -> { + // 3. ensure one open connection + long openSocketsAfter = getNumberOfOpenClients(socketSpy); + assertThat(openSocketsAfter, is(equalTo(0L))); + }, 60_000, 50); - // change callback should have been called twice (countdown at zero) - assertThat(unexpectedCount.get(), is(equalTo(0))); - assertThat(expectedCount.get(), is(equalTo(2))); - assertThat(callbackCalled.getCount(), is(equalTo(0L))); + } } - @Test - public void testGetRegisteredRegularPolls() { - ModbusSlaveEndpoint endpoint = getEndpoint(); - BasicPollTaskImpl task = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 15, 1), null); - BasicPollTaskImpl task2 = new BasicPollTaskImpl(endpoint, new BasicModbusReadRequestBlueprint(SLAVE_UNIT_ID, - ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 1, 16, 2), null); - - modbusManager.registerRegularPoll(task, 50, 0); - modbusManager.registerRegularPoll(task2, 50, 0); - assertThat(modbusManager.getRegisteredRegularPolls(), - is(equalTo(Stream.of(task, task2).collect(Collectors.toSet())))); - modbusManager.unregisterRegularPoll(task); - assertThat(modbusManager.getRegisteredRegularPolls(), - is(equalTo(Stream.of(task2).collect(Collectors.toSet())))); + private long getNumberOfOpenClients(SpyingSocketFactory socketSpy) { + final InetAddress testServerAddress; + try { + testServerAddress = localAddress(); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + return socketSpy.sockets.stream().filter(socketImpl -> { + Socket socket = getSocketOfSocketImpl(socketImpl); + return socket.getPort() == tcpModbusPort && socket.isConnected() + && socket.getLocalAddress().equals(testServerAddress); + }).count(); + } + + /** + * Spy all sockets that are created + * + * @author Sami Salonen + * + */ + private static class SpyingSocketFactory implements SocketImplFactory { + + Queue sockets = new ConcurrentLinkedQueue(); + + @Override + public SocketImpl createSocketImpl() { + SocketImpl socket = newSocksSocketImpl(); + sockets.add(socket); + return socket; + } + } + + private static SocketImpl newSocksSocketImpl() { + try { + Class defaultSocketImpl = Class.forName("java.net.SocksSocketImpl"); + Constructor constructor = defaultSocketImpl.getDeclaredConstructor(); + constructor.setAccessible(true); + return (SocketImpl) constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Get Socket corresponding to SocketImpl using reflection + */ + private static Socket getSocketOfSocketImpl(SocketImpl impl) { + try { + Method getSocket = SocketImpl.class.getDeclaredMethod("getSocket"); + getSocket.setAccessible(true); + return (Socket) getSocket.invoke(impl); + } catch (Exception e) { + throw new RuntimeException(e); + } } } diff --git a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/AbstractModbusOSGiTest.java b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/AbstractModbusOSGiTest.java index c77acb6cf3dc0..64cd7406352ef 100644 --- a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/AbstractModbusOSGiTest.java +++ b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/AbstractModbusOSGiTest.java @@ -16,7 +16,8 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.*; -import static org.mockito.Mockito.reset; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; import java.util.ArrayList; import java.util.Collections; @@ -57,6 +58,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.openhab.binding.modbus.internal.ModbusHandlerFactory; +import org.openhab.io.transport.modbus.ModbusCommunicationInterface; import org.openhab.io.transport.modbus.ModbusManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -98,27 +100,22 @@ public void receive(Event event) { private final Logger logger = LoggerFactory.getLogger(AbstractModbusOSGiTest.class); @Mock - @NonNullByDefault({}) - protected ModbusManager mockedModbusManager; - - @NonNullByDefault({}) - protected ManagedThingProvider thingProvider; - @NonNullByDefault({}) - protected ManagedItemProvider itemProvider; - @NonNullByDefault({}) - protected ManagedItemChannelLinkProvider itemChannelLinkProvider; - @NonNullByDefault({}) - protected ItemRegistry itemRegistry; - @NonNullByDefault({}) - protected CoreItemFactory coreItemFactory; - - @NonNullByDefault({}) - private ModbusManager realModbusManager; + protected @NonNullByDefault({}) ModbusManager mockedModbusManager; + protected @NonNullByDefault({}) ManagedThingProvider thingProvider; + protected @NonNullByDefault({}) ManagedItemProvider itemProvider; + protected @NonNullByDefault({}) ManagedItemChannelLinkProvider itemChannelLinkProvider; + protected @NonNullByDefault({}) ItemRegistry itemRegistry; + protected @NonNullByDefault({}) CoreItemFactory coreItemFactory; + + private @NonNullByDefault({}) ModbusManager realModbusManager; private Set addedItems = new HashSet<>(); private Set addedThings = new HashSet<>(); private Set addedLinks = new HashSet<>(); private StateSubscriber stateSubscriber = new StateSubscriber(); + @Mock + protected @NonNullByDefault({}) ModbusCommunicationInterface comms; + public AbstractModbusOSGiTest() { super(); } @@ -148,6 +145,7 @@ public void setUpAbstractModbusOSGiTest() { // Clean slate for all tests reset(mockedModbusManager); + stateSubscriber.stateUpdates.clear(); logger.debug("setUpAbstractModbusOSGiTest END"); } @@ -219,7 +217,12 @@ protected void mockTransformation(String name, TransformationService service) { registerService(service, params); } - private void swapModbusManagerToMocked() { + protected void mockCommsToModbusManager() { + assert comms != null; + doReturn(comms).when(mockedModbusManager).newModbusCommunicationInterface(any(), any()); + } + + protected void swapModbusManagerToMocked() { assertNull(realModbusManager); realModbusManager = getService(ModbusManager.class); assertThat("Could not get ModbusManager", realModbusManager, is(notNullValue())); @@ -234,7 +237,7 @@ private void swapModbusManagerToMocked() { modbusHandlerFactory.setModbusManager(mockedModbusManager); } - private void swapModbusManagerToReal() { + protected void swapModbusManagerToReal() { assertNotNull(realModbusManager); ModbusHandlerFactory modbusHandlerFactory = getService(ThingHandlerFactory.class, ModbusHandlerFactory.class); assertThat("Could not get ModbusHandlerFactory", modbusHandlerFactory, is(notNullValue())); diff --git a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java index b1ddf0f2ea45f..7b3e4cab07f85 100644 --- a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java +++ b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusDataHandlerTest.java @@ -14,6 +14,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*; @@ -27,7 +28,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Supplier; import org.apache.commons.lang.StringUtils; import org.eclipse.smarthome.config.core.Configuration; @@ -61,16 +61,15 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.openhab.binding.modbus.handler.ModbusPollerThingHandler; import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler; -import org.openhab.binding.modbus.internal.handler.ModbusPollerThingHandler; -import org.openhab.binding.modbus.internal.handler.ModbusPollerThingHandlerImpl; import org.openhab.binding.modbus.internal.handler.ModbusTcpThingHandler; -import org.openhab.io.transport.modbus.BasicModbusRegister; -import org.openhab.io.transport.modbus.BasicModbusRegisterArray; +import org.openhab.io.transport.modbus.AsyncModbusFailure; +import org.openhab.io.transport.modbus.AsyncModbusReadResult; +import org.openhab.io.transport.modbus.AsyncModbusWriteResult; import org.openhab.io.transport.modbus.BitArray; import org.openhab.io.transport.modbus.ModbusConstants; import org.openhab.io.transport.modbus.ModbusConstants.ValueType; -import org.openhab.io.transport.modbus.ModbusManager; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; import org.openhab.io.transport.modbus.ModbusRegister; @@ -81,7 +80,6 @@ import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint; import org.openhab.io.transport.modbus.ModbusWriteRequestBlueprint; import org.openhab.io.transport.modbus.PollTask; -import org.openhab.io.transport.modbus.WriteTask; import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; import org.openhab.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint; import org.osgi.framework.BundleContext; @@ -114,17 +112,17 @@ public String transform(String function, String source) throws TransformationExc CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_WRITE_ERROR, "DateTime"); CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_READ_ERROR, "DateTime"); } - private List writeTasks = new ArrayList<>(); + private List writeRequests = new ArrayList<>(); @After public void tearDown() { - writeTasks.clear(); + writeRequests.clear(); } private void captureModbusWrites() { - Mockito.when(mockedModbusManager.submitOneTimeWrite(any())).then(invocation -> { - WriteTask task = (WriteTask) invocation.getArgument(0); - writeTasks.add(task); + Mockito.when(comms.submitOneTimeWrite(any(), any(), any())).then(invocation -> { + ModbusWriteRequestBlueprint task = (ModbusWriteRequestBlueprint) invocation.getArgument(0); + writeRequests.add(task); return Mockito.mock(ScheduledFuture.class); }); } @@ -143,13 +141,16 @@ private Bridge createPollerMock(String pollerId, PollTask task) { poller = builder.build(); poller.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, "")); - ModbusPollerThingHandlerImpl mockHandler = Mockito.mock(ModbusPollerThingHandlerImpl.class); - doReturn(task).when(mockHandler).getPollTask(); - Supplier managerRef = () -> mockedModbusManager; - doReturn(managerRef).when(mockHandler).getManagerRef(); + ModbusPollerThingHandler mockHandler = Mockito.mock(ModbusPollerThingHandler.class); + doReturn(task.getRequest()).when(mockHandler).getRequest(); + assert comms != null; + doReturn(comms).when(mockHandler).getCommunicationInterface(); + doReturn(task.getEndpoint()).when(comms).getEndpoint(); poller.setHandler(mockHandler); assertSame(poller.getHandler(), mockHandler); - assertSame(((ModbusPollerThingHandlerImpl) poller.getHandler()).getPollTask(), task); + assertSame(((ModbusPollerThingHandler) poller.getHandler()).getCommunicationInterface().getEndpoint(), + task.getEndpoint()); + assertSame(((ModbusPollerThingHandler) poller.getHandler()).getRequest(), task.getRequest()); addThing(poller); return poller; @@ -161,10 +162,8 @@ private Bridge createTcpMock() { ModbusTcpThingHandler tcpThingHandler = Mockito.mock(ModbusTcpThingHandler.class); tcpBridge.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, "")); tcpBridge.setHandler(tcpThingHandler); - Supplier managerRef = () -> mockedModbusManager; - doReturn(managerRef).when(tcpThingHandler).getManagerRef(); + doReturn(comms).when(tcpThingHandler).getCommunicationInterface(); doReturn(0).when(tcpThingHandler).getSlaveId(); - doReturn(endpoint).when(tcpThingHandler).asSlaveEndpoint(); tcpThingHandler.initialize(); assertThat(tcpBridge.getStatus(), is(equalTo(ThingStatus.ONLINE))); return tcpBridge; @@ -424,16 +423,20 @@ private ModbusDataThingHandler testReadHandlingGeneric(ModbusReadFunctionCode fu if (bits != null) { assertNull(registers); assertNull(error); - dataHandler.onBits(request, bits); + AsyncModbusReadResult result = new AsyncModbusReadResult(request, bits); + dataHandler.onReadResult(result); } else if (registers != null) { assertNull(bits); assertNull(error); - dataHandler.onRegisters(request, registers); + AsyncModbusReadResult result = new AsyncModbusReadResult(request, registers); + dataHandler.onReadResult(result); } else { assertNull(bits); assertNull(registers); assertNotNull(error); - dataHandler.onError(request, error); + AsyncModbusFailure result = new AsyncModbusFailure( + request, error); + dataHandler.handleReadError(result); } return dataHandler; } @@ -470,7 +473,7 @@ private ModbusDataThingHandler testWriteHandlingGeneric(String start, String tra dataHandler.handleCommand(new ChannelUID(dataHandler.getThing().getUID(), channel), command); if (error != null) { - dataHandler.onError(request, error); + dataHandler.handleReadError(new AsyncModbusFailure(request, error)); } else { ModbusResponse resp = new ModbusResponse() { @@ -479,7 +482,8 @@ public int getFunctionCode() { return successFC.getFunctionCode(); } }; - dataHandler.onWriteResponse(Mockito.mock(ModbusWriteRequestBlueprint.class), resp); + dataHandler + .onWriteResponse(new AsyncModbusWriteResult(Mockito.mock(ModbusWriteRequestBlueprint.class), resp)); } return dataHandler; } @@ -495,9 +499,8 @@ public void testOnError() { @Test public void testOnRegistersInt16StaticTransformation() { ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, - "0", "-3", ModbusConstants.ValueType.INT16, null, new BasicModbusRegisterArray( - new ModbusRegister[] { new BasicModbusRegister((byte) 0xff, (byte) 0xfd) }), - null); + "0", "-3", ModbusConstants.ValueType.INT16, null, + new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister((byte) 0xff, (byte) 0xfd) }), null); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class))); @@ -518,9 +521,8 @@ public void testOnRegistersRealTransformation() { mockTransformation("MULTIPLY", new MultiplyTransformation()); ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, "0", "MULTIPLY(10)", ModbusConstants.ValueType.INT16, null, - new BasicModbusRegisterArray( - new ModbusRegister[] { new BasicModbusRegister((byte) 0xff, (byte) 0xfd) }), - null, bundleContext); + new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister((byte) 0xff, (byte) 0xfd) }), null, + bundleContext); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class))); @@ -540,10 +542,10 @@ public void testOnRegistersRealTransformation() { @Test public void testOnRegistersNaNFloatInRegisters() throws InvalidSyntaxException { ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, - "0", "default", ModbusConstants.ValueType.FLOAT32, null, new BasicModbusRegisterArray( + "0", "default", ModbusConstants.ValueType.FLOAT32, null, new ModbusRegisterArray( // equivalent of floating point NaN - new ModbusRegister[] { new BasicModbusRegister((byte) 0x7f, (byte) 0xc0), - new BasicModbusRegister((byte) 0x00, (byte) 0x00) }), + new ModbusRegister[] { new ModbusRegister((byte) 0x7f, (byte) 0xc0), + new ModbusRegister((byte) 0x00, (byte) 0x00) }), null, bundleContext); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class))); @@ -569,9 +571,8 @@ public String transform(String function, String source) throws TransformationExc }); ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, "0", "ONOFF(10)", ModbusConstants.ValueType.INT16, null, - new BasicModbusRegisterArray( - new ModbusRegister[] { new BasicModbusRegister((byte) 0xff, (byte) 0xfd) }), - null, bundleContext); + new ModbusRegisterArray(new ModbusRegister[] { new ModbusRegister((byte) 0xff, (byte) 0xfd) }), null, + bundleContext); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class))); @@ -594,13 +595,13 @@ public void testWriteRealTransformation() throws InvalidSyntaxException { assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class))); - assertThat(writeTasks.size(), is(equalTo(1))); - WriteTask writeTask = writeTasks.get(0); - assertThat(writeTask.getRequest().getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL))); - assertThat(writeTask.getRequest().getReference(), is(equalTo(50))); - assertThat(((ModbusWriteCoilRequestBlueprint) writeTask.getRequest()).getCoils().size(), is(equalTo(1))); + assertThat(writeRequests.size(), is(equalTo(1))); + ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0); + assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL))); + assertThat(writeRequest.getReference(), is(equalTo(50))); + assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1))); // Since transform output is non-zero, it is mapped as "true" - assertThat(((ModbusWriteCoilRequestBlueprint) writeTask.getRequest()).getCoils().getBit(0), is(equalTo(true))); + assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(true))); } @Test @@ -619,13 +620,13 @@ public String transform(String function, String source) throws TransformationExc assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class))); - assertThat(writeTasks.size(), is(equalTo(1))); - WriteTask writeTask = writeTasks.get(0); - assertThat(writeTask.getRequest().getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL))); - assertThat(writeTask.getRequest().getReference(), is(equalTo(50))); - assertThat(((ModbusWriteCoilRequestBlueprint) writeTask.getRequest()).getCoils().size(), is(equalTo(1))); + assertThat(writeRequests.size(), is(equalTo(1))); + ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0); + assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL))); + assertThat(writeRequest.getReference(), is(equalTo(50))); + assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1))); // Since transform output is zero, it is mapped as "false" - assertThat(((ModbusWriteCoilRequestBlueprint) writeTask.getRequest()).getCoils().getBit(0), is(equalTo(false))); + assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(false))); } @Test @@ -644,15 +645,12 @@ public String transform(String function, String source) throws TransformationExc assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class))); - assertThat(writeTasks.size(), is(equalTo(1))); - WriteTask writeTask = writeTasks.get(0); - assertThat(writeTask.getRequest().getFunctionCode(), - is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER))); - assertThat(writeTask.getRequest().getReference(), is(equalTo(50))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeTask.getRequest()).getRegisters().size(), - is(equalTo(1))); - assertThat( - ((ModbusWriteRegisterRequestBlueprint) writeTask.getRequest()).getRegisters().getRegister(0).getValue(), + assertThat(writeRequests.size(), is(equalTo(1))); + ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0); + assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER))); + assertThat(writeRequest.getReference(), is(equalTo(50))); + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1))); + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0).getValue(), is(equalTo(5))); } @@ -681,30 +679,26 @@ public String transform(String function, String source) throws TransformationExc assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class))); assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class))); - assertThat(writeTasks.size(), is(equalTo(2))); + assertThat(writeRequests.size(), is(equalTo(2))); { - WriteTask writeTask = writeTasks.get(0); - assertThat(writeTask.getRequest().getFunctionCode(), - is(equalTo(ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS))); - assertThat(writeTask.getRequest().getReference(), is(equalTo(5412))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeTask.getRequest()).getRegisters().size(), - is(equalTo(3))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeTask.getRequest()).getRegisters().getRegister(0) - .getValue(), is(equalTo(1))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeTask.getRequest()).getRegisters().getRegister(1) - .getValue(), is(equalTo(0))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeTask.getRequest()).getRegisters().getRegister(2) - .getValue(), is(equalTo(5))); + ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0); + assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS))); + assertThat(writeRequest.getReference(), is(equalTo(5412))); + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(3))); + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0).getValue(), + is(equalTo(1))); + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(1).getValue(), + is(equalTo(0))); + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(2).getValue(), + is(equalTo(5))); } { - WriteTask writeTask = writeTasks.get(1); - assertThat(writeTask.getRequest().getFunctionCode(), - is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER))); - assertThat(writeTask.getRequest().getReference(), is(equalTo(555))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeTask.getRequest()).getRegisters().size(), - is(equalTo(1))); - assertThat(((ModbusWriteRegisterRequestBlueprint) writeTask.getRequest()).getRegisters().getRegister(0) - .getValue(), is(equalTo(3))); + ModbusWriteRequestBlueprint writeRequest = writeRequests.get(1); + assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER))); + assertThat(writeRequest.getReference(), is(equalTo(555))); + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1))); + assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0).getValue(), + is(equalTo(3))); } } @@ -784,7 +778,7 @@ public void testRefreshOnData() throws InterruptedException { builder -> builder.withConfiguration(dataConfig), bundleContext); assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE))); - verify(mockedModbusManager, never()).submitOneTimePoll(task); + verify(comms, never()).submitOneTimePoll(eq(request), notNull(), notNull()); // Reset initial REFRESH commands to data thing channels from the Core reset(poller.getHandler()); dataHandler.handleCommand(Mockito.mock(ChannelUID.class), RefreshType.REFRESH); diff --git a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusPollerThingHandlerTest.java b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusPollerThingHandlerTest.java index 9adc03215764c..9d3a4cd09e24a 100644 --- a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusPollerThingHandlerTest.java +++ b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusPollerThingHandlerTest.java @@ -14,7 +14,6 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsSame.sameInstance; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -22,7 +21,6 @@ import java.lang.reflect.Field; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; import org.eclipse.smarthome.config.core.Configuration; import org.eclipse.smarthome.core.thing.Bridge; @@ -34,7 +32,6 @@ import org.eclipse.smarthome.core.thing.binding.ThingHandlerCallback; import org.eclipse.smarthome.core.thing.binding.builder.BridgeBuilder; import org.hamcrest.Description; -import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.junit.After; import org.junit.Before; @@ -44,17 +41,20 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.openhab.binding.modbus.handler.ModbusPollerThingHandler; import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal; import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler; -import org.openhab.binding.modbus.internal.handler.ModbusPollerThingHandler; -import org.openhab.binding.modbus.internal.handler.ModbusPollerThingHandlerImpl; -import org.openhab.binding.modbus.internal.handler.ModbusTcpThingHandler; +import org.openhab.io.transport.modbus.AsyncModbusFailure; +import org.openhab.io.transport.modbus.AsyncModbusReadResult; import org.openhab.io.transport.modbus.BitArray; +import org.openhab.io.transport.modbus.ModbusFailureCallback; import org.openhab.io.transport.modbus.ModbusReadCallback; import org.openhab.io.transport.modbus.ModbusReadFunctionCode; import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint; import org.openhab.io.transport.modbus.ModbusRegisterArray; import org.openhab.io.transport.modbus.PollTask; +import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint; +import org.openhab.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +64,9 @@ @RunWith(MockitoJUnitRunner.class) public class ModbusPollerThingHandlerTest extends AbstractModbusOSGiTest { + private static final String HOST = "thisishost"; + private static final int PORT = 44; + private final Logger logger = LoggerFactory.getLogger(ModbusPollerThingHandlerTest.class); private Bridge endpoint; @@ -90,25 +93,32 @@ public static BridgeBuilder createPollerThingBuilder(String id) { * Verify that basic poller <-> endpoint interaction has taken place (on poller init) */ private void verifyEndpointBasicInitInteraction() { - verify(mockedModbusManager, times(1)).addListener(any()); - verify(mockedModbusManager, times(1)).setEndpointPoolConfiguration(any(), any()); + verify(mockedModbusManager).newModbusCommunicationInterface(any(), any()); } public ModbusReadCallback getPollerCallback(ModbusPollerThingHandler handler) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { - Field callbackField = ModbusPollerThingHandlerImpl.class.getDeclaredField("callbackDelegator"); + Field callbackField = ModbusPollerThingHandler.class.getDeclaredField("callbackDelegator"); callbackField.setAccessible(true); return (ModbusReadCallback) callbackField.get(handler); } + public ModbusFailureCallback getPollerFailureCallback(ModbusPollerThingHandler handler) + throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + Field callbackField = ModbusPollerThingHandler.class.getDeclaredField("callbackDelegator"); + callbackField.setAccessible(true); + return (ModbusFailureCallback) callbackField.get(handler); + } + /** * Before each test, setup TCP endpoint thing, configure mocked item registry */ @Before public void setUp() { + mockCommsToModbusManager(); Configuration tcpConfig = new Configuration(); - tcpConfig.put("host", "thisishost"); - tcpConfig.put("port", 44); + tcpConfig.put("host", HOST); + tcpConfig.put("port", PORT); tcpConfig.put("id", 9); endpoint = createTcpThingBuilder("tcpendpoint").withConfiguration(tcpConfig).build(); addThing(endpoint); @@ -147,8 +157,11 @@ public void testInitializeNonPolling() verifyNoMoreInteractions(mockedModbusManager); } - public void testPollingGeneric(String type, Supplier> pollTaskMatcherSupplier) + public void testPollingGeneric(String type, ModbusReadFunctionCode expectedFunctionCode) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + PollTask pollTask = Mockito.mock(PollTask.class); + doReturn(pollTask).when(comms).registerRegularPoll(notNull(), eq(150l), eq(0L), notNull(), notNull()); + Configuration pollerConfig = new Configuration(); pollerConfig.put("refresh", 150L); pollerConfig.put("start", 5); @@ -158,60 +171,77 @@ public void testPollingGeneric(String type, Supplier> pollTask .build(); addThing(poller); - assertThat(poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); - verify(mockedModbusManager).registerRegularPoll(argThat(pollTaskMatcherSupplier.get()), eq(150l), eq(0L)); + assertThat(poller.getStatusInfo().toString(), poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); + verifyEndpointBasicInitInteraction(); - verifyNoMoreInteractions(mockedModbusManager); - } + verify(mockedModbusManager).newModbusCommunicationInterface(argThat(new TypeSafeMatcher() { - private boolean checkPollTask(PollTask item, ModbusReadFunctionCode functionCode) { - ModbusTcpThingHandler endPointHandler = (ModbusTcpThingHandler) endpoint.getHandler(); - assertNotNull(endPointHandler); - return item.getEndpoint().equals(endPointHandler.asSlaveEndpoint()) && item.getRequest().getDataLength() == 13 - && item.getRequest().getFunctionCode() == functionCode && item.getRequest().getProtocolID() == 0 - && item.getRequest().getReference() == 5 && item.getRequest().getUnitID() == 9; - } + @Override + public void describeTo(Description description) { + description.appendText("correct endpoint ("); + } - Matcher isRequestOkGeneric(ModbusReadFunctionCode functionCode) { - return new TypeSafeMatcher() { @Override - public boolean matchesSafely(PollTask item) { - return checkPollTask(item, functionCode); + protected boolean matchesSafely(ModbusSlaveEndpoint endpoint) { + return checkEndpoint(endpoint); } + }), any()); + + verify(comms).registerRegularPoll(argThat(new TypeSafeMatcher() { @Override public void describeTo(Description description) { + description.appendText("correct request"); + } + + @Override + protected boolean matchesSafely(ModbusReadRequestBlueprint request) { + return checkRequest(request, expectedFunctionCode); } - }; + }), eq(150l), eq(0L), notNull(), notNull()); + verifyNoMoreInteractions(mockedModbusManager); + } + + @SuppressWarnings("null") + private boolean checkEndpoint(ModbusSlaveEndpoint endpointParam) { + return endpointParam.equals(new ModbusTCPSlaveEndpoint(HOST, PORT)); + } + + private boolean checkRequest(ModbusReadRequestBlueprint request, ModbusReadFunctionCode functionCode) { + return request.getDataLength() == 13 && request.getFunctionCode() == functionCode + && request.getProtocolID() == 0 && request.getReference() == 5 && request.getUnitID() == 9; } @Test public void testInitializePollingWithCoils() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - testPollingGeneric("coil", () -> isRequestOkGeneric(ModbusReadFunctionCode.READ_COILS)); + testPollingGeneric("coil", ModbusReadFunctionCode.READ_COILS); } @Test public void testInitializePollingWithDiscrete() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - testPollingGeneric("discrete", () -> isRequestOkGeneric(ModbusReadFunctionCode.READ_INPUT_DISCRETES)); + testPollingGeneric("discrete", ModbusReadFunctionCode.READ_INPUT_DISCRETES); } @Test public void testInitializePollingWithInputRegisters() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - testPollingGeneric("input", () -> isRequestOkGeneric(ModbusReadFunctionCode.READ_INPUT_REGISTERS)); + testPollingGeneric("input", ModbusReadFunctionCode.READ_INPUT_REGISTERS); } @Test public void testInitializePollingWithHoldingRegisters() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - testPollingGeneric("holding", () -> isRequestOkGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS)); + testPollingGeneric("holding", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS); } @Test public void testPollUnregistrationOnDispose() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + PollTask pollTask = Mockito.mock(PollTask.class); + doReturn(pollTask).when(comms).registerRegularPoll(notNull(), eq(150l), eq(0L), notNull(), notNull()); + Configuration pollerConfig = new Configuration(); pollerConfig.put("refresh", 150L); pollerConfig.put("start", 5); @@ -224,39 +254,50 @@ public void testPollUnregistrationOnDispose() // verify registration final AtomicReference callbackRef = new AtomicReference<>(); - verify(mockedModbusManager).registerRegularPoll(argThat(new TypeSafeMatcher() { + verify(mockedModbusManager).newModbusCommunicationInterface(argThat(new TypeSafeMatcher() { @Override public void describeTo(Description description) { + description.appendText("correct endpoint"); } @Override - protected boolean matchesSafely(PollTask item) { - callbackRef.set(item.getCallback()); - return checkPollTask(item, ModbusReadFunctionCode.READ_COILS); + protected boolean matchesSafely(ModbusSlaveEndpoint endpoint) { + return checkEndpoint(endpoint); } - }), eq(150l), eq(0L)); - verifyNoMoreInteractions(mockedModbusManager); + }), any()); + verify(comms).registerRegularPoll(argThat(new TypeSafeMatcher() { - // reset call counts for easy assertions - reset(mockedModbusManager); - - // remove the thing - disposeThing(poller); + @Override + public void describeTo(Description description) { + } - // 1) should first unregister poll task - verify(mockedModbusManager).unregisterRegularPoll(argThat(new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(ModbusReadRequestBlueprint request) { + return checkRequest(request, ModbusReadFunctionCode.READ_COILS); + } + }), eq(150l), eq(0L), argThat(new TypeSafeMatcher() { @Override public void describeTo(Description description) { } @Override - protected boolean matchesSafely(PollTask item) { - assertThat(item.getCallback(), is(sameInstance(callbackRef.get()))); - return checkPollTask(item, ModbusReadFunctionCode.READ_COILS); + protected boolean matchesSafely(ModbusReadCallback callback) { + callbackRef.set(callback); + return true; } - })); + }), notNull()); + verifyNoMoreInteractions(mockedModbusManager); + + // reset call counts for easy assertions + reset(mockedModbusManager); + + // remove the thing + disposeThing(poller); + + // 1) should first unregister poll task + verify(comms).unregisterRegularPoll(eq(pollTask)); verifyNoMoreInteractions(mockedModbusManager); } @@ -303,6 +344,9 @@ public void testInitializeWithOfflineBridge() @Test public void testRegistersPassedToChildDataThings() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + PollTask pollTask = Mockito.mock(PollTask.class); + doReturn(pollTask).when(comms).registerRegularPoll(notNull(), eq(150l), eq(0L), notNull(), notNull()); + Configuration pollerConfig = new Configuration(); pollerConfig.put("refresh", 150L); pollerConfig.put("start", 5); @@ -315,25 +359,27 @@ public void testRegistersPassedToChildDataThings() assertThat(poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); - ArgumentCaptor pollTaskCapturer = ArgumentCaptor.forClass(PollTask.class); - verify(mockedModbusManager).registerRegularPoll(pollTaskCapturer.capture(), eq(150l), eq(0L)); - ModbusReadCallback readCallback = pollTaskCapturer.getValue().getCallback(); + ArgumentCaptor callbackCapturer = ArgumentCaptor.forClass(ModbusReadCallback.class); + verify(comms).registerRegularPoll(notNull(), eq(150l), eq(0L), callbackCapturer.capture(), notNull()); + ModbusReadCallback readCallback = callbackCapturer.getValue(); assertNotNull(readCallback); ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class); ModbusRegisterArray registers = Mockito.mock(ModbusRegisterArray.class); - ModbusPollerThingHandlerImpl thingHandler = (ModbusPollerThingHandlerImpl) poller.getHandler(); + ModbusPollerThingHandler thingHandler = (ModbusPollerThingHandler) poller.getHandler(); assertNotNull(thingHandler); ModbusDataThingHandler child1 = Mockito.mock(ModbusDataThingHandler.class); ModbusDataThingHandler child2 = Mockito.mock(ModbusDataThingHandler.class); + AsyncModbusReadResult result = new AsyncModbusReadResult(request, registers); + // has one data child thingHandler.childHandlerInitialized(child1, Mockito.mock(Thing.class)); - readCallback.onRegisters(request, registers); - verify(child1).onRegisters(request, registers); + readCallback.handle(result); + verify(child1).onReadResult(result); verifyNoMoreInteractions(child1); verifyNoMoreInteractions(child2); @@ -341,9 +387,9 @@ public void testRegistersPassedToChildDataThings() // two children (one child initialized) thingHandler.childHandlerInitialized(child2, Mockito.mock(Thing.class)); - readCallback.onRegisters(request, registers); - verify(child1).onRegisters(request, registers); - verify(child2).onRegisters(request, registers); + readCallback.handle(result); + verify(child1).onReadResult(result); + verify(child2).onReadResult(result); verifyNoMoreInteractions(child1); verifyNoMoreInteractions(child2); @@ -352,8 +398,8 @@ public void testRegistersPassedToChildDataThings() // one child disposed thingHandler.childHandlerDisposed(child1, Mockito.mock(Thing.class)); - readCallback.onRegisters(request, registers); - verify(child2).onRegisters(request, registers); + readCallback.handle(result); + verify(child2).onReadResult(result); verifyNoMoreInteractions(child1); verifyNoMoreInteractions(child2); } @@ -361,6 +407,9 @@ public void testRegistersPassedToChildDataThings() @Test public void testBitsPassedToChildDataThings() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + PollTask pollTask = Mockito.mock(PollTask.class); + doReturn(pollTask).when(comms).registerRegularPoll(notNull(), eq(150l), eq(0L), notNull(), notNull()); + Configuration pollerConfig = new Configuration(); pollerConfig.put("refresh", 150L); pollerConfig.put("start", 5); @@ -373,25 +422,27 @@ public void testBitsPassedToChildDataThings() assertThat(poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); - ArgumentCaptor pollTaskCapturer = ArgumentCaptor.forClass(PollTask.class); - verify(mockedModbusManager).registerRegularPoll(pollTaskCapturer.capture(), eq(150l), eq(0L)); - ModbusReadCallback readCallback = pollTaskCapturer.getValue().getCallback(); + ArgumentCaptor callbackCapturer = ArgumentCaptor.forClass(ModbusReadCallback.class); + verify(comms).registerRegularPoll(any(), eq(150l), eq(0L), callbackCapturer.capture(), notNull()); + ModbusReadCallback readCallback = callbackCapturer.getValue(); assertNotNull(readCallback); ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class); BitArray bits = Mockito.mock(BitArray.class); - ModbusPollerThingHandlerImpl thingHandler = (ModbusPollerThingHandlerImpl) poller.getHandler(); + ModbusPollerThingHandler thingHandler = (ModbusPollerThingHandler) poller.getHandler(); assertNotNull(thingHandler); ModbusDataThingHandler child1 = Mockito.mock(ModbusDataThingHandler.class); ModbusDataThingHandler child2 = Mockito.mock(ModbusDataThingHandler.class); + AsyncModbusReadResult result = new AsyncModbusReadResult(request, bits); + // has one data child thingHandler.childHandlerInitialized(child1, Mockito.mock(Thing.class)); - readCallback.onBits(request, bits); - verify(child1).onBits(request, bits); + readCallback.handle(result); + verify(child1).onReadResult(result); verifyNoMoreInteractions(child1); verifyNoMoreInteractions(child2); @@ -399,9 +450,9 @@ public void testBitsPassedToChildDataThings() // two children (one child initialized) thingHandler.childHandlerInitialized(child2, Mockito.mock(Thing.class)); - readCallback.onBits(request, bits); - verify(child1).onBits(request, bits); - verify(child2).onBits(request, bits); + readCallback.handle(result); + verify(child1).onReadResult(result); + verify(child2).onReadResult(result); verifyNoMoreInteractions(child1); verifyNoMoreInteractions(child2); @@ -410,8 +461,8 @@ public void testBitsPassedToChildDataThings() // one child disposed thingHandler.childHandlerDisposed(child1, Mockito.mock(Thing.class)); - readCallback.onBits(request, bits); - verify(child2).onBits(request, bits); + readCallback.handle(result); + verify(child2).onReadResult(result); verifyNoMoreInteractions(child1); verifyNoMoreInteractions(child2); } @@ -419,6 +470,9 @@ public void testBitsPassedToChildDataThings() @Test public void testErrorPassedToChildDataThings() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + PollTask pollTask = Mockito.mock(PollTask.class); + doReturn(pollTask).when(comms).registerRegularPoll(notNull(), eq(150l), eq(0L), notNull(), notNull()); + Configuration pollerConfig = new Configuration(); pollerConfig.put("refresh", 150L); pollerConfig.put("start", 5); @@ -431,25 +485,29 @@ public void testErrorPassedToChildDataThings() assertThat(poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); - ArgumentCaptor pollTaskCapturer = ArgumentCaptor.forClass(PollTask.class); - verify(mockedModbusManager).registerRegularPoll(pollTaskCapturer.capture(), eq(150l), eq(0L)); - ModbusReadCallback readCallback = pollTaskCapturer.getValue().getCallback(); + final ArgumentCaptor> callbackCapturer = ArgumentCaptor + .forClass((Class) ModbusFailureCallback.class); + verify(comms).registerRegularPoll(any(), eq(150l), eq(0L), notNull(), callbackCapturer.capture()); + ModbusFailureCallback readCallback = callbackCapturer.getValue(); assertNotNull(readCallback); ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class); Exception error = Mockito.mock(Exception.class); - ModbusPollerThingHandlerImpl thingHandler = (ModbusPollerThingHandlerImpl) poller.getHandler(); + ModbusPollerThingHandler thingHandler = (ModbusPollerThingHandler) poller.getHandler(); assertNotNull(thingHandler); ModbusDataThingHandler child1 = Mockito.mock(ModbusDataThingHandler.class); ModbusDataThingHandler child2 = Mockito.mock(ModbusDataThingHandler.class); + AsyncModbusFailure result = new AsyncModbusFailure( + request, error); + // has one data child thingHandler.childHandlerInitialized(child1, Mockito.mock(Thing.class)); - readCallback.onError(request, error); - verify(child1).onError(request, error); + readCallback.handle(result); + verify(child1).handleReadError(result); verifyNoMoreInteractions(child1); verifyNoMoreInteractions(child2); @@ -457,9 +515,9 @@ public void testErrorPassedToChildDataThings() // two children (one child initialized) thingHandler.childHandlerInitialized(child2, Mockito.mock(Thing.class)); - readCallback.onError(request, error); - verify(child1).onError(request, error); - verify(child2).onError(request, error); + readCallback.handle(result); + verify(child1).handleReadError(result); + verify(child2).handleReadError(result); verifyNoMoreInteractions(child1); verifyNoMoreInteractions(child2); @@ -468,8 +526,8 @@ public void testErrorPassedToChildDataThings() // one child disposed thingHandler.childHandlerDisposed(child1, Mockito.mock(Thing.class)); - readCallback.onError(request, error); - verify(child2).onError(request, error); + readCallback.handle(result); + verify(child2).handleReadError(result); verifyNoMoreInteractions(child1); verifyNoMoreInteractions(child2); } @@ -489,11 +547,11 @@ public void testRefresh() assertThat(poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); - verify(mockedModbusManager, never()).submitOneTimePoll(any()); - ModbusPollerThingHandlerImpl thingHandler = (ModbusPollerThingHandlerImpl) poller.getHandler(); + verify(comms, never()).submitOneTimePoll(any(), any(), any()); + ModbusPollerThingHandler thingHandler = (ModbusPollerThingHandler) poller.getHandler(); assertNotNull(thingHandler); thingHandler.refresh(); - verify(mockedModbusManager).submitOneTimePoll(any()); + verify(comms).submitOneTimePoll(any(), any(), any()); } /** @@ -519,32 +577,33 @@ public void testRefreshWithPreviousData() verifyEndpointBasicInitInteraction(); ModbusDataThingHandler child1 = Mockito.mock(ModbusDataThingHandler.class); - ModbusPollerThingHandlerImpl thingHandler = (ModbusPollerThingHandlerImpl) poller.getHandler(); + ModbusPollerThingHandler thingHandler = (ModbusPollerThingHandler) poller.getHandler(); assertNotNull(thingHandler); thingHandler.childHandlerInitialized(child1, Mockito.mock(Thing.class)); assertThat(poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); - verify(mockedModbusManager, never()).submitOneTimePoll(any()); + verify(comms, never()).submitOneTimePoll(any(), any(), any()); // data is received ModbusReadCallback pollerReadCallback = getPollerCallback(thingHandler); ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class); ModbusRegisterArray registers = Mockito.mock(ModbusRegisterArray.class); - pollerReadCallback.onRegisters(request, registers); + AsyncModbusReadResult result = new AsyncModbusReadResult(request, registers); + pollerReadCallback.handle(result); // data child receives the data - verify(child1).onRegisters(request, registers); + verify(child1).onReadResult(result); verifyNoMoreInteractions(child1); reset(child1); // call refresh // cache is still valid, we should not have real data poll this time thingHandler.refresh(); - verify(mockedModbusManager, never()).submitOneTimePoll(any()); + verify(comms, never()).submitOneTimePoll(any(), any(), any()); // data child receives the cached data - verify(child1).onRegisters(request, registers); + verify(child1).onReadResult(result); verifyNoMoreInteractions(child1); } @@ -570,30 +629,32 @@ public void testRefreshWithPreviousDataCacheDisabled() addThing(poller); verifyEndpointBasicInitInteraction(); - ModbusPollerThingHandlerImpl thingHandler = (ModbusPollerThingHandlerImpl) poller.getHandler(); + ModbusPollerThingHandler thingHandler = (ModbusPollerThingHandler) poller.getHandler(); assertNotNull(thingHandler); ModbusDataThingHandler child1 = Mockito.mock(ModbusDataThingHandler.class); thingHandler.childHandlerInitialized(child1, Mockito.mock(Thing.class)); assertThat(poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); - verify(mockedModbusManager, never()).submitOneTimePoll(any()); + verify(comms, never()).submitOneTimePoll(any(), any(), any()); // data is received ModbusReadCallback pollerReadCallback = getPollerCallback(thingHandler); ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class); ModbusRegisterArray registers = Mockito.mock(ModbusRegisterArray.class); - pollerReadCallback.onRegisters(request, registers); + AsyncModbusReadResult result = new AsyncModbusReadResult(request, registers); + + pollerReadCallback.handle(result); // data child receives the data - verify(child1).onRegisters(request, registers); + verify(child1).onReadResult(result); verifyNoMoreInteractions(child1); reset(child1); // call refresh // caching disabled, should poll from manager thingHandler.refresh(); - verify(mockedModbusManager).submitOneTimePoll(any()); + verify(comms).submitOneTimePoll(any(), any(), any()); verifyNoMoreInteractions(mockedModbusManager); // data child receives the cached data @@ -623,26 +684,30 @@ public void testRefreshWithPreviousData2() throws IllegalArgumentException, Ille addThing(poller); verifyEndpointBasicInitInteraction(); - ModbusPollerThingHandlerImpl thingHandler = (ModbusPollerThingHandlerImpl) poller.getHandler(); + ModbusPollerThingHandler thingHandler = (ModbusPollerThingHandler) poller.getHandler(); assertNotNull(thingHandler); ModbusDataThingHandler child1 = Mockito.mock(ModbusDataThingHandler.class); thingHandler.childHandlerInitialized(child1, Mockito.mock(Thing.class)); assertThat(poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); - verify(mockedModbusManager, never()).submitOneTimePoll(any()); + verify(comms, never()).submitOneTimePoll(any(), any(), any()); // data is received ModbusReadCallback pollerReadCallback = getPollerCallback(thingHandler); + ModbusFailureCallback failureCallback = getPollerFailureCallback(thingHandler); ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class); ModbusReadRequestBlueprint request2 = Mockito.mock(ModbusReadRequestBlueprint.class); ModbusRegisterArray registers = Mockito.mock(ModbusRegisterArray.class); Exception error = Mockito.mock(Exception.class); + AsyncModbusReadResult registersResult = new AsyncModbusReadResult(request, registers); + AsyncModbusFailure errorResult = new AsyncModbusFailure( + request2, error); - pollerReadCallback.onRegisters(request, registers); + pollerReadCallback.handle(registersResult); // data child should receive the data - verify(child1).onRegisters(request, registers); + verify(child1).onReadResult(registersResult); verifyNoMoreInteractions(child1); reset(child1); @@ -650,26 +715,27 @@ public void testRefreshWithPreviousData2() throws IllegalArgumentException, Ille Thread.sleep(5L); // error is received - pollerReadCallback.onError(request2, error); + failureCallback.handle(errorResult); // data child should receive the error - verify(child1).onError(request2, error); + verify(child1).handleReadError(errorResult); verifyNoMoreInteractions(child1); reset(child1); // call refresh, should return latest data (that is, error) // cache is still valid, we should not have real data poll this time thingHandler.refresh(); - verify(mockedModbusManager, never()).submitOneTimePoll(any()); + verify(comms, never()).submitOneTimePoll(any(), any(), any()); // data child receives the cached error - verify(child1).onError(request2, error); + verify(child1).handleReadError(errorResult); verifyNoMoreInteractions(child1); } @Test public void testRefreshWithOldPreviousData() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException, InterruptedException { + Configuration pollerConfig = new Configuration(); pollerConfig.put("refresh", 0L); pollerConfig.put("start", 5); @@ -681,24 +747,25 @@ public void testRefreshWithOldPreviousData() throws IllegalArgumentException, Il addThing(poller); verifyEndpointBasicInitInteraction(); - ModbusPollerThingHandlerImpl thingHandler = (ModbusPollerThingHandlerImpl) poller.getHandler(); + ModbusPollerThingHandler thingHandler = (ModbusPollerThingHandler) poller.getHandler(); assertNotNull(thingHandler); ModbusDataThingHandler child1 = Mockito.mock(ModbusDataThingHandler.class); thingHandler.childHandlerInitialized(child1, Mockito.mock(Thing.class)); assertThat(poller.getStatus(), is(equalTo(ThingStatus.ONLINE))); - verify(mockedModbusManager, never()).submitOneTimePoll(any()); + verify(comms, never()).submitOneTimePoll(any(), any(), any()); // data is received ModbusReadCallback pollerReadCallback = getPollerCallback(thingHandler); ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class); ModbusRegisterArray registers = Mockito.mock(ModbusRegisterArray.class); + AsyncModbusReadResult result = new AsyncModbusReadResult(request, registers); - pollerReadCallback.onRegisters(request, registers); + pollerReadCallback.handle(result); // data child should receive the data - verify(child1).onRegisters(request, registers); + verify(child1).onReadResult(result); verifyNoMoreInteractions(child1); reset(child1); @@ -706,8 +773,8 @@ public void testRefreshWithOldPreviousData() throws IllegalArgumentException, Il Thread.sleep(15L); // call refresh. Since cache expired, will poll for more - verify(mockedModbusManager, never()).submitOneTimePoll(any()); + verify(comms, never()).submitOneTimePoll(any(), any(), any()); thingHandler.refresh(); - verify(mockedModbusManager).submitOneTimePoll(any()); + verify(comms).submitOneTimePoll(any(), any(), any()); } } diff --git a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusTcpThingHandlerTest.java b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusTcpThingHandlerTest.java index a54bc497d0ca6..e93dfaf46ec1d 100644 --- a/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusTcpThingHandlerTest.java +++ b/itests/org.openhab.binding.modbus.tests/src/main/java/org/openhab/binding/modbus/tests/ModbusTcpThingHandlerTest.java @@ -70,82 +70,115 @@ public void testInitializeAndSlaveEndpoint() { ModbusTcpThingHandler thingHandler = (ModbusTcpThingHandler) thing.getHandler(); assertNotNull(thingHandler); - ModbusSlaveEndpoint slaveEndpoint = thingHandler.asSlaveEndpoint(); + ModbusSlaveEndpoint slaveEndpoint = thingHandler.getEndpoint(); assertThat(slaveEndpoint, is(equalTo(new ModbusTCPSlaveEndpoint("thisishost", 44)))); assertThat(thingHandler.getSlaveId(), is(9)); InOrder orderedVerify = Mockito.inOrder(mockedModbusManager); - orderedVerify.verify(mockedModbusManager).addListener(thingHandler); - ModbusSlaveEndpoint endpoint = thingHandler.asSlaveEndpoint(); + ModbusSlaveEndpoint endpoint = thingHandler.getEndpoint(); Objects.requireNonNull(endpoint); - orderedVerify.verify(mockedModbusManager).setEndpointPoolConfiguration(endpoint, expectedPoolConfiguration); + orderedVerify.verify(mockedModbusManager).newModbusCommunicationInterface(endpoint, expectedPoolConfiguration); } @Test public void testTwoDifferentEndpointWithDifferentParameters() { // thing1 - Configuration thingConfig = new Configuration(); - thingConfig.put("host", "thisishost"); - thingConfig.put("port", 44); - thingConfig.put("connectMaxTries", 1); - thingConfig.put("timeBetweenTransactionsMillis", 1); - - final Bridge thing = createTcpThingBuilder("tcpendpoint").withConfiguration(thingConfig).build(); - addThing(thing); - assertThat(thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); - - ModbusTcpThingHandler thingHandler = (ModbusTcpThingHandler) thing.getHandler(); - assertNotNull(thingHandler); - EndpointPoolConfiguration poolConfiguration = new EndpointPoolConfiguration(); - poolConfiguration.setInterTransactionDelayMillis(2); - // Different endpoint (port 45), so should not affect this thing - thingHandler.onEndpointPoolConfigurationSet(new ModbusTCPSlaveEndpoint("thisishost", 45), poolConfiguration); - assertThat(thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + { + Configuration thingConfig = new Configuration(); + thingConfig.put("host", "thisishost"); + thingConfig.put("port", 44); + thingConfig.put("connectMaxTries", 1); + thingConfig.put("timeBetweenTransactionsMillis", 1); + + final Bridge thing = createTcpThingBuilder("tcpendpoint").withConfiguration(thingConfig).build(); + addThing(thing); + assertThat(thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + + ModbusTcpThingHandler thingHandler = (ModbusTcpThingHandler) thing.getHandler(); + assertNotNull(thingHandler); + } + { + Configuration thingConfig = new Configuration(); + thingConfig.put("host", "thisishost"); + thingConfig.put("port", 45); + thingConfig.put("connectMaxTries", 1); + thingConfig.put("timeBetweenTransactionsMillis", 100); + + final Bridge thing = createTcpThingBuilder("tcpendpoint2").withConfiguration(thingConfig).build(); + addThing(thing); + // Different endpoint (port 45), so should be OK even though timeBetweenTransactionsMillis is different + assertThat(thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + + ModbusTcpThingHandler thingHandler = (ModbusTcpThingHandler) thing.getHandler(); + assertNotNull(thingHandler); + } } @Test public void testTwoIdenticalEndpointWithDifferentParameters() { + // Real implementation needed to validate this behaviour + swapModbusManagerToReal(); // thing1 - Configuration thingConfig = new Configuration(); - thingConfig.put("host", "thisishost"); - thingConfig.put("port", 44); - thingConfig.put("connectMaxTries", 1); - thingConfig.put("timeBetweenTransactionsMillis", 1); - - final Bridge thing = createTcpThingBuilder("tcpendpoint").withConfiguration(thingConfig).build(); - addThing(thing); - assertThat(thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); - - ModbusTcpThingHandler thingHandler = (ModbusTcpThingHandler) thing.getHandler(); - assertNotNull(thingHandler); - EndpointPoolConfiguration poolConfiguration = new EndpointPoolConfiguration(); - poolConfiguration.setInterTransactionDelayMillis(2); - // Same endpoint and different parameters -> OFFLINE - thingHandler.onEndpointPoolConfigurationSet(new ModbusTCPSlaveEndpoint("thisishost", 44), poolConfiguration); - assertThat(thing.getStatus(), is(equalTo(ThingStatus.OFFLINE))); - assertThat(thing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR))); + { + Configuration thingConfig = new Configuration(); + thingConfig.put("host", "thisishost"); + thingConfig.put("port", 44); + thingConfig.put("connectMaxTries", 1); + thingConfig.put("timeBetweenTransactionsMillis", 1); + + final Bridge thing = createTcpThingBuilder("tcpendpoint").withConfiguration(thingConfig).build(); + addThing(thing); + assertThat(thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + + ModbusTcpThingHandler thingHandler = (ModbusTcpThingHandler) thing.getHandler(); + assertNotNull(thingHandler); + } + { + + Configuration thingConfig = new Configuration(); + thingConfig.put("host", "thisishost"); + thingConfig.put("port", 44); + thingConfig.put("connectMaxTries", 1); + thingConfig.put("timeBetweenTransactionsMillis", 100); + + final Bridge thing = createTcpThingBuilder("tcpendpoint2").withConfiguration(thingConfig).build(); + addThing(thing); + assertThat(thing.getStatus(), is(equalTo(ThingStatus.OFFLINE))); + assertThat(thing.getStatusInfo().getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR))); + } } @Test public void testTwoIdenticalEndpointWithSameParameters() { + // Real implementation needed to validate this behaviour + swapModbusManagerToReal(); // thing1 - Configuration thingConfig = new Configuration(); - thingConfig.put("host", "thisishost"); - thingConfig.put("port", 44); - thingConfig.put("connectMaxTries", 1); - thingConfig.put("timeBetweenTransactionsMillis", 1); - - final Bridge thing = createTcpThingBuilder("tcpendpoint").withConfiguration(thingConfig).build(); - addThing(thing); - assertThat(thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); - - ModbusTcpThingHandler thingHandler = (ModbusTcpThingHandler) thing.getHandler(); - assertNotNull(thingHandler); - EndpointPoolConfiguration poolConfiguration = new EndpointPoolConfiguration(); - poolConfiguration.setInterTransactionDelayMillis(1); - poolConfiguration.setConnectTimeoutMillis(10000); // default timeout - // Same endpoint and same parameters -> should not affect this thing - thingHandler.onEndpointPoolConfigurationSet(new ModbusTCPSlaveEndpoint("thisishost", 44), poolConfiguration); - assertThat(thing.getStatusInfo().getDescription(), thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + { + Configuration thingConfig = new Configuration(); + thingConfig.put("host", "thisishost"); + thingConfig.put("port", 44); + thingConfig.put("connectMaxTries", 1); + thingConfig.put("timeBetweenTransactionsMillis", 1); + + final Bridge thing = createTcpThingBuilder("tcpendpoint").withConfiguration(thingConfig).build(); + addThing(thing); + assertThat(thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + + ModbusTcpThingHandler thingHandler = (ModbusTcpThingHandler) thing.getHandler(); + assertNotNull(thingHandler); + } + { + Configuration thingConfig = new Configuration(); + thingConfig.put("host", "thisishost"); + thingConfig.put("port", 44); + thingConfig.put("connectMaxTries", 1); + thingConfig.put("timeBetweenTransactionsMillis", 1); + thingConfig.put("connectTimeoutMillis", 10000); // default + + final Bridge thing = createTcpThingBuilder("tcpendpoint2").withConfiguration(thingConfig).build(); + addThing(thing); + // Same endpoint and same parameters -> should not affect this thing + assertThat(thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + } } }