2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.modbus.tests;
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.*;
18 import static org.mockito.ArgumentMatchers.*;
19 import static org.mockito.ArgumentMatchers.any;
20 import static org.mockito.Mockito.*;
21 import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*;
23 import java.lang.reflect.Field;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.HashMap;
27 import java.util.List;
29 import java.util.Map.Entry;
30 import java.util.Objects;
31 import java.util.concurrent.ScheduledFuture;
32 import java.util.function.Consumer;
33 import java.util.function.Function;
34 import java.util.stream.Stream;
36 import org.hamcrest.Matcher;
37 import org.junit.jupiter.api.AfterEach;
38 import org.junit.jupiter.api.BeforeEach;
39 import org.junit.jupiter.api.Test;
40 import org.junit.jupiter.params.ParameterizedTest;
41 import org.junit.jupiter.params.provider.Arguments;
42 import org.junit.jupiter.params.provider.CsvSource;
43 import org.junit.jupiter.params.provider.MethodSource;
44 import org.mockito.Mockito;
45 import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
46 import org.openhab.binding.modbus.handler.ModbusPollerThingHandler;
47 import org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal;
48 import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler;
49 import org.openhab.binding.modbus.internal.handler.ModbusTcpThingHandler;
50 import org.openhab.core.config.core.Configuration;
51 import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
52 import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
53 import org.openhab.core.io.transport.modbus.AsyncModbusWriteResult;
54 import org.openhab.core.io.transport.modbus.BitArray;
55 import org.openhab.core.io.transport.modbus.ModbusConstants;
56 import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
57 import org.openhab.core.io.transport.modbus.ModbusReadCallback;
58 import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
59 import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
60 import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
61 import org.openhab.core.io.transport.modbus.ModbusResponse;
62 import org.openhab.core.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
63 import org.openhab.core.io.transport.modbus.ModbusWriteFunctionCode;
64 import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
65 import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
66 import org.openhab.core.io.transport.modbus.PollTask;
67 import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
68 import org.openhab.core.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
69 import org.openhab.core.items.GenericItem;
70 import org.openhab.core.items.Item;
71 import org.openhab.core.library.types.DecimalType;
72 import org.openhab.core.library.types.OnOffType;
73 import org.openhab.core.library.types.OpenClosedType;
74 import org.openhab.core.library.types.StringType;
75 import org.openhab.core.thing.Bridge;
76 import org.openhab.core.thing.ChannelUID;
77 import org.openhab.core.thing.Thing;
78 import org.openhab.core.thing.ThingStatus;
79 import org.openhab.core.thing.ThingStatusDetail;
80 import org.openhab.core.thing.ThingStatusInfo;
81 import org.openhab.core.thing.ThingUID;
82 import org.openhab.core.thing.binding.builder.BridgeBuilder;
83 import org.openhab.core.thing.binding.builder.ChannelBuilder;
84 import org.openhab.core.thing.binding.builder.ThingBuilder;
85 import org.openhab.core.transform.TransformationException;
86 import org.openhab.core.transform.TransformationService;
87 import org.openhab.core.types.Command;
88 import org.openhab.core.types.RefreshType;
89 import org.openhab.core.types.State;
90 import org.openhab.core.types.UnDefType;
91 import org.osgi.framework.BundleContext;
92 import org.osgi.framework.InvalidSyntaxException;
95 * @author Sami Salonen - Initial contribution
97 public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
99 private final class MultiplyTransformation implements TransformationService {
101 public String transform(String function, String source) throws TransformationException {
102 return String.valueOf(Integer.parseInt(function) * Integer.parseInt(source));
106 private static final String HOST = "thisishost";
107 private static final int PORT = 44;
109 private static final Map<String, String> CHANNEL_TO_ACCEPTED_TYPE = new HashMap<>();
111 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_SWITCH, "Switch");
112 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_CONTACT, "Contact");
113 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_DATETIME, "DateTime");
114 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_DIMMER, "Dimmer");
115 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_NUMBER, "Number");
116 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_STRING, "String");
117 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_ROLLERSHUTTER, "Rollershutter");
118 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_READ_SUCCESS, "DateTime");
119 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_WRITE_SUCCESS, "DateTime");
120 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_WRITE_ERROR, "DateTime");
121 CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_READ_ERROR, "DateTime");
123 private List<ModbusWriteRequestBlueprint> writeRequests = new ArrayList<>();
124 private Bridge realEndpointWithMockedComms;
126 public ModbusReadCallback getPollerCallback(ModbusPollerThingHandler handler) {
129 callbackField = ModbusPollerThingHandler.class.getDeclaredField("callbackDelegator");
130 callbackField.setAccessible(true);
131 return (ModbusReadCallback) callbackField.get(handler);
132 } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
134 throw new RuntimeException(e);
139 public void beforeEach() {
140 mockCommsToModbusManager();
141 Configuration tcpConfig = new Configuration();
142 tcpConfig.put("host", HOST);
143 tcpConfig.put("port", PORT);
144 tcpConfig.put("id", 9);
146 realEndpointWithMockedComms = BridgeBuilder
147 .create(ModbusBindingConstantsInternal.THING_TYPE_MODBUS_TCP,
148 new ThingUID(ModbusBindingConstantsInternal.THING_TYPE_MODBUS_TCP, "mytcp"))
149 .withLabel("label for mytcp").withConfiguration(tcpConfig).build();
150 addThing(realEndpointWithMockedComms);
151 assertEquals(ThingStatus.ONLINE, realEndpointWithMockedComms.getStatus(),
152 realEndpointWithMockedComms.getStatusInfo().getDescription());
156 public void tearDown() {
157 writeRequests.clear();
158 if (realEndpointWithMockedComms != null) {
159 thingProvider.remove(realEndpointWithMockedComms.getUID());
163 private static Arguments appendArg(Arguments args, Object obj) {
164 Object[] newArgs = Arrays.copyOf(args.get(), args.get().length + 1);
165 newArgs[args.get().length] = obj;
166 return Arguments.of(newArgs);
169 private void captureModbusWrites() {
170 Mockito.when(comms.submitOneTimeWrite(any(), any(), any())).then(invocation -> {
171 ModbusWriteRequestBlueprint task = (ModbusWriteRequestBlueprint) invocation.getArgument(0);
172 writeRequests.add(task);
173 return Mockito.mock(ScheduledFuture.class);
177 private Bridge createPollerMock(String pollerId, PollTask task) {
179 ThingUID thingUID = new ThingUID(THING_TYPE_MODBUS_POLLER, pollerId);
180 BridgeBuilder builder = BridgeBuilder.create(THING_TYPE_MODBUS_POLLER, thingUID)
181 .withLabel("label for " + pollerId);
182 for (Entry<String, String> entry : CHANNEL_TO_ACCEPTED_TYPE.entrySet()) {
183 String channelId = entry.getKey();
184 String channelAcceptedType = entry.getValue();
185 builder = builder.withChannel(
186 ChannelBuilder.create(new ChannelUID(thingUID, channelId), channelAcceptedType).build());
188 poller = builder.build();
189 poller.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
191 ModbusPollerThingHandler mockHandler = Mockito.mock(ModbusPollerThingHandler.class);
192 doReturn(task.getRequest()).when(mockHandler).getRequest();
193 assert comms != null;
194 doReturn(comms).when(mockHandler).getCommunicationInterface();
195 doReturn(task.getEndpoint()).when(comms).getEndpoint();
196 poller.setHandler(mockHandler);
197 assertSame(poller.getHandler(), mockHandler);
198 assertSame(((ModbusPollerThingHandler) poller.getHandler()).getCommunicationInterface().getEndpoint(),
200 assertSame(((ModbusPollerThingHandler) poller.getHandler()).getRequest(), task.getRequest());
206 private Bridge createTcpMock() {
207 Bridge tcpBridge = ModbusPollerThingHandlerTest.createTcpThingBuilder("tcp1").build();
208 ModbusTcpThingHandler tcpThingHandler = Mockito.mock(ModbusTcpThingHandler.class);
209 tcpBridge.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
210 tcpBridge.setHandler(tcpThingHandler);
211 doReturn(comms).when(tcpThingHandler).getCommunicationInterface();
213 doReturn(0).when(tcpThingHandler).getSlaveId();
214 } catch (EndpointNotInitializedException e) {
215 // not raised -- we are mocking return value only, not actually calling the method
216 throw new IllegalStateException();
218 tcpThingHandler.initialize();
219 assertThat(tcpBridge.getStatus(), is(equalTo(ThingStatus.ONLINE)));
223 private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
224 Function<ThingBuilder, ThingBuilder> builderConfigurator) {
225 return createDataHandler(id, bridge, builderConfigurator, null);
228 private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
229 Function<ThingBuilder, ThingBuilder> builderConfigurator, BundleContext context) {
230 return createDataHandler(id, bridge, builderConfigurator, context, true);
233 private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
234 Function<ThingBuilder, ThingBuilder> builderConfigurator, BundleContext context,
235 boolean autoCreateItemsAndLinkToChannels) {
236 ThingUID thingUID = new ThingUID(THING_TYPE_MODBUS_DATA, id);
237 ThingBuilder builder = ThingBuilder.create(THING_TYPE_MODBUS_DATA, thingUID).withLabel("label for " + id);
238 Map<String, ChannelUID> toBeLinked = new HashMap<>();
239 for (Entry<String, String> entry : CHANNEL_TO_ACCEPTED_TYPE.entrySet()) {
240 String channelId = entry.getKey();
241 // accepted item type
242 String channelAcceptedType = entry.getValue();
243 ChannelUID channelUID = new ChannelUID(thingUID, channelId);
244 builder = builder.withChannel(ChannelBuilder.create(channelUID, channelAcceptedType).build());
246 if (autoCreateItemsAndLinkToChannels) {
247 // Create item of correct type and link it to channel
248 String itemName = getItemName(channelUID);
249 final GenericItem item;
250 item = coreItemFactory.createItem(channelAcceptedType, itemName);
251 assertThat(String.format("Could not determine correct item type for %s", channelId), item,
254 Objects.requireNonNull(item);
256 toBeLinked.put(itemName, channelUID);
259 if (builderConfigurator != null) {
260 builder = builderConfigurator.apply(builder);
263 Thing dataThing = builder.withBridge(bridge.getUID()).build();
266 // Link after the things and items have been created
267 for (Entry<String, ChannelUID> entry : toBeLinked.entrySet()) {
268 linkItem(entry.getKey(), entry.getValue());
270 return (ModbusDataThingHandler) dataThing.getHandler();
273 private String getItemName(ChannelUID channelUID) {
274 return channelUID.toString().replace(':', '_') + "_item";
277 private void assertSingleStateUpdate(ModbusDataThingHandler handler, String channel, Matcher<State> matcher) {
278 waitForAssert(() -> {
279 ChannelUID channelUID = new ChannelUID(handler.getThing().getUID(), channel);
280 String itemName = getItemName(channelUID);
281 Item item = itemRegistry.get(itemName);
282 assertThat(String.format("Item %s is not available from item registry", itemName), item,
285 List<State> updates = getStateUpdates(itemName);
286 if (updates != null) {
288 String.format("Many updates found, expected one: %s", Arrays.deepToString(updates.toArray())),
289 updates.size(), is(equalTo(1)));
291 State state = updates == null ? null : updates.get(0);
292 assertThat(String.format("%s %s, state %s of type %s", item.getClass().getSimpleName(), itemName, state,
293 state == null ? null : state.getClass().getSimpleName()), state, is(matcher));
297 private void assertSingleStateUpdate(ModbusDataThingHandler handler, String channel, State state) {
298 assertSingleStateUpdate(handler, channel, is(equalTo(state)));
301 private void testOutOfBoundsGeneric(int pollStart, int pollLength, String start,
302 ModbusReadFunctionCode functionCode, ValueType valueType, ThingStatus expectedStatus) {
303 testOutOfBoundsGeneric(pollStart, pollLength, start, functionCode, valueType, expectedStatus, null);
306 private void testOutOfBoundsGeneric(int pollStart, int pollLength, String start,
307 ModbusReadFunctionCode functionCode, ValueType valueType, ThingStatus expectedStatus,
308 BundleContext context) {
309 ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
311 // Minimally mocked request
312 ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
313 doReturn(pollStart).when(request).getReference();
314 doReturn(pollLength).when(request).getDataLength();
315 doReturn(functionCode).when(request).getFunctionCode();
317 PollTask task = Mockito.mock(PollTask.class);
318 doReturn(endpoint).when(task).getEndpoint();
319 doReturn(request).when(task).getRequest();
321 Bridge pollerThing = createPollerMock("poller1", task);
323 Configuration dataConfig = new Configuration();
324 dataConfig.put("readStart", start);
325 dataConfig.put("readTransform", "default");
326 dataConfig.put("readValueType", valueType.getConfigValue());
327 ModbusDataThingHandler dataHandler = createDataHandler("data1", pollerThing,
328 builder -> builder.withConfiguration(dataConfig), context);
329 assertThat(dataHandler.getThing().getStatusInfo().getDescription(), dataHandler.getThing().getStatus(),
330 is(equalTo(expectedStatus)));
334 public void testInitCoilsOutOfIndex() {
335 testOutOfBoundsGeneric(4, 3, "8", ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT,
336 ThingStatus.OFFLINE);
340 public void testInitCoilsOutOfIndex2() {
341 // Reading coils 4, 5, 6. Coil 7 is out of bounds
342 testOutOfBoundsGeneric(4, 3, "7", ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT,
343 ThingStatus.OFFLINE);
347 public void testInitCoilsOK() {
348 // Reading coils 4, 5, 6. Coil 6 is OK
349 testOutOfBoundsGeneric(4, 3, "6", ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT,
354 public void testInitRegistersWithBitOutOfIndex() {
355 testOutOfBoundsGeneric(4, 3, "8.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
356 ModbusConstants.ValueType.BIT, ThingStatus.OFFLINE);
360 public void testInitRegistersWithBitOutOfIndex2() {
361 testOutOfBoundsGeneric(4, 3, "7.16", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
362 ModbusConstants.ValueType.BIT, ThingStatus.OFFLINE);
366 public void testInitRegistersWithBitOK() {
367 testOutOfBoundsGeneric(4, 3, "6.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
368 ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
372 public void testInitRegistersWithBitOK2() {
373 testOutOfBoundsGeneric(4, 3, "6.15", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
374 ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
378 public void testInitRegistersWithInt8OutOfIndex() {
379 testOutOfBoundsGeneric(4, 3, "8.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
380 ModbusConstants.ValueType.INT8, ThingStatus.OFFLINE);
384 public void testInitRegistersWithInt8OutOfIndex2() {
385 testOutOfBoundsGeneric(4, 3, "7.2", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
386 ModbusConstants.ValueType.INT8, ThingStatus.OFFLINE);
390 public void testInitRegistersWithInt8OK() {
391 testOutOfBoundsGeneric(4, 3, "6.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
392 ModbusConstants.ValueType.INT8, ThingStatus.ONLINE);
396 public void testInitRegistersWithInt8OK2() {
397 testOutOfBoundsGeneric(4, 3, "6.1", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
398 ModbusConstants.ValueType.INT8, ThingStatus.ONLINE);
402 public void testInitRegistersWithInt16OK() {
403 // Poller reading registers 4, 5, 6. Register 6 is OK
404 testOutOfBoundsGeneric(4, 3, "6", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
405 ModbusConstants.ValueType.INT16, ThingStatus.ONLINE);
409 public void testInitRegistersWithInt16OutOfBounds() {
410 // Poller reading registers 4, 5, 6. Register 7 is out-of-bounds
411 testOutOfBoundsGeneric(4, 3, "7", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
412 ModbusConstants.ValueType.INT16, ThingStatus.OFFLINE);
416 public void testInitRegistersWithInt16OutOfBounds2() {
417 testOutOfBoundsGeneric(4, 3, "8", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
418 ModbusConstants.ValueType.INT16, ThingStatus.OFFLINE);
422 public void testInitRegistersWithInt16NoDecimalFormatAllowed() {
423 testOutOfBoundsGeneric(4, 3, "7.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
424 ModbusConstants.ValueType.INT16, ThingStatus.OFFLINE);
428 public void testInitRegistersWithInt32OK() {
429 testOutOfBoundsGeneric(4, 3, "5", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
430 ModbusConstants.ValueType.INT32, ThingStatus.ONLINE);
434 public void testInitRegistersWithInt32OutOfBounds() {
435 testOutOfBoundsGeneric(4, 3, "6", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
436 ModbusConstants.ValueType.INT32, ThingStatus.OFFLINE);
440 public void testInitRegistersWithInt32AtTheEdge() {
441 testOutOfBoundsGeneric(4, 3, "5", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
442 ModbusConstants.ValueType.INT32, ThingStatus.ONLINE);
445 private ModbusDataThingHandler testReadHandlingGeneric(ModbusReadFunctionCode functionCode, String start,
446 String transform, ValueType valueType, BitArray bits, ModbusRegisterArray registers, Exception error) {
447 return testReadHandlingGeneric(functionCode, start, transform, valueType, bits, registers, error, null);
450 private ModbusDataThingHandler testReadHandlingGeneric(ModbusReadFunctionCode functionCode, String start,
451 String transform, ValueType valueType, BitArray bits, ModbusRegisterArray registers, Exception error,
452 BundleContext context) {
453 return testReadHandlingGeneric(functionCode, start, transform, valueType, bits, registers, error, context,
457 @SuppressWarnings({ "null" })
458 private ModbusDataThingHandler testReadHandlingGeneric(ModbusReadFunctionCode functionCode, String start,
459 String transform, ValueType valueType, BitArray bits, ModbusRegisterArray registers, Exception error,
460 BundleContext context, boolean autoCreateItemsAndLinkToChannels) {
461 ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
465 // Minimally mocked request
466 ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
467 doReturn(pollLength).when(request).getDataLength();
468 doReturn(functionCode).when(request).getFunctionCode();
470 PollTask task = Mockito.mock(PollTask.class);
471 doReturn(endpoint).when(task).getEndpoint();
472 doReturn(request).when(task).getRequest();
474 Bridge poller = createPollerMock("poller1", task);
476 Configuration dataConfig = new Configuration();
477 dataConfig.put("readStart", start);
478 dataConfig.put("readTransform", transform);
479 dataConfig.put("readValueType", valueType.getConfigValue());
481 String thingId = "read1";
482 ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
483 builder -> builder.withConfiguration(dataConfig), context, autoCreateItemsAndLinkToChannels);
485 assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
489 assertNull(registers);
491 AsyncModbusReadResult result = new AsyncModbusReadResult(request, bits);
492 dataHandler.onReadResult(result);
493 } else if (registers != null) {
496 AsyncModbusReadResult result = new AsyncModbusReadResult(request, registers);
497 dataHandler.onReadResult(result);
500 assertNull(registers);
501 assertNotNull(error);
502 AsyncModbusFailure<ModbusReadRequestBlueprint> result = new AsyncModbusFailure<ModbusReadRequestBlueprint>(
504 dataHandler.handleReadError(result);
509 private ModbusDataThingHandler testWriteHandlingGeneric(String start, String transform, ValueType valueType,
510 String writeType, ModbusWriteFunctionCode successFC, String channel, Command command, Exception error,
511 BundleContext context) {
512 return testWriteHandlingGeneric(start, transform, valueType, writeType, successFC, channel, command, error,
516 @SuppressWarnings({ "null" })
517 private ModbusDataThingHandler testWriteHandlingGeneric(String start, String transform, ValueType valueType,
518 String writeType, ModbusWriteFunctionCode successFC, String channel, Command command, Exception error,
519 BundleContext context, boolean parentIsEndpoint) {
520 ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
522 // Minimally mocked request
523 ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
525 PollTask task = Mockito.mock(PollTask.class);
526 doReturn(endpoint).when(task).getEndpoint();
527 doReturn(request).when(task).getRequest();
530 if (parentIsEndpoint) {
531 parent = createTcpMock();
534 parent = createPollerMock("poller1", task);
537 Configuration dataConfig = new Configuration();
538 dataConfig.put("readStart", "");
539 dataConfig.put("writeStart", start);
540 dataConfig.put("writeTransform", transform);
541 dataConfig.put("writeValueType", valueType.getConfigValue());
542 dataConfig.put("writeType", writeType);
544 String thingId = "write";
546 ModbusDataThingHandler dataHandler = createDataHandler(thingId, parent,
547 builder -> builder.withConfiguration(dataConfig), context);
549 assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
551 dataHandler.handleCommand(new ChannelUID(dataHandler.getThing().getUID(), channel), command);
554 dataHandler.handleReadError(new AsyncModbusFailure<ModbusReadRequestBlueprint>(request, error));
556 ModbusResponse resp = new ModbusResponse() {
559 public int getFunctionCode() {
560 return successFC.getFunctionCode();
564 .onWriteResponse(new AsyncModbusWriteResult(Mockito.mock(ModbusWriteRequestBlueprint.class), resp));
570 public void testOnError() {
571 ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
572 "0.0", "default", ModbusConstants.ValueType.BIT, null, null, new Exception("fooerror"));
574 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(notNullValue(State.class)));
578 public void testOnRegistersInt16StaticTransformation() {
579 ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
580 "0", "-3", ModbusConstants.ValueType.INT16, null,
581 new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null);
583 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
584 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
586 // -3 converts to "true"
587 assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, is(nullValue(State.class)));
588 assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, is(nullValue(State.class)));
589 assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, is(nullValue(State.class)));
590 assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, new DecimalType(-3));
591 // roller shutter fails since -3 is invalid value (not between 0...100)
592 // assertThatStateContains(state, CHANNEL_ROLLERSHUTTER, new PercentType(1));
593 assertSingleStateUpdate(dataHandler, CHANNEL_STRING, new StringType("-3"));
594 // no datetime, conversion not possible without transformation
598 public void testOnRegistersRealTransformation() {
599 mockTransformation("MULTIPLY", new MultiplyTransformation());
600 ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
601 "0", "MULTIPLY(10)", ModbusConstants.ValueType.INT16, null,
602 new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null, bundleContext);
604 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
605 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
607 // transformation output (-30) is not valid for contact or switch
608 assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, is(nullValue(State.class)));
609 assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, is(nullValue(State.class)));
610 // -30 is not valid value for Dimmer (PercentType) (not between 0...100)
611 assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, is(nullValue(State.class)));
612 assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, new DecimalType(-30));
613 // roller shutter fails since -3 is invalid value (not between 0...100)
614 assertSingleStateUpdate(dataHandler, CHANNEL_ROLLERSHUTTER, is(nullValue(State.class)));
615 assertSingleStateUpdate(dataHandler, CHANNEL_STRING, new StringType("-30"));
616 // no datetime, conversion not possible without transformation
620 public void testOnRegistersNaNFloatInRegisters() throws InvalidSyntaxException {
621 ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
622 "0", "default", ModbusConstants.ValueType.FLOAT32, null, new ModbusRegisterArray(
623 // equivalent of floating point NaN
624 new byte[] { (byte) 0x7f, (byte) 0xc0, (byte) 0x00, (byte) 0x00 }),
625 null, bundleContext);
627 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
628 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
630 // UNDEF is treated as "boolean true" (OPEN/ON) since it is != 0.
631 assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, OpenClosedType.OPEN);
632 assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, OnOffType.ON);
633 assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, OnOffType.ON);
634 assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, UnDefType.UNDEF);
635 assertSingleStateUpdate(dataHandler, CHANNEL_ROLLERSHUTTER, UnDefType.UNDEF);
636 assertSingleStateUpdate(dataHandler, CHANNEL_STRING, UnDefType.UNDEF);
640 public void testOnRegistersRealTransformation2() throws InvalidSyntaxException {
641 mockTransformation("ONOFF", new TransformationService() {
644 public String transform(String function, String source) throws TransformationException {
645 return Integer.parseInt(source) != 0 ? "ON" : "OFF";
648 ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
649 "0", "ONOFF(10)", ModbusConstants.ValueType.INT16, null,
650 new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null, bundleContext);
652 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
653 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
655 assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, is(nullValue(State.class)));
656 assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, is(equalTo(OnOffType.ON)));
657 assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, is(equalTo(OnOffType.ON)));
658 assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, is(nullValue(State.class)));
659 assertSingleStateUpdate(dataHandler, CHANNEL_ROLLERSHUTTER, is(nullValue(State.class)));
660 assertSingleStateUpdate(dataHandler, CHANNEL_STRING, is(equalTo(new StringType("ON"))));
664 public void testWriteWithDataAsChildOfEndpoint() throws InvalidSyntaxException {
665 captureModbusWrites();
666 mockTransformation("MULTIPLY", new MultiplyTransformation());
667 ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "MULTIPLY(10)",
668 ModbusConstants.ValueType.BIT, "coil", ModbusWriteFunctionCode.WRITE_COIL, "number",
669 new DecimalType("2"), null, bundleContext, /* parent is endpoint */true);
671 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
672 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
673 assertThat(writeRequests.size(), is(equalTo(1)));
674 ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
675 assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL)));
676 assertThat(writeRequest.getReference(), is(equalTo(50)));
677 assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1)));
678 // Since transform output is non-zero, it is mapped as "true"
679 assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(true)));
683 public void testWriteRealTransformation() throws InvalidSyntaxException {
684 captureModbusWrites();
685 mockTransformation("MULTIPLY", new MultiplyTransformation());
686 ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "MULTIPLY(10)",
687 ModbusConstants.ValueType.BIT, "coil", ModbusWriteFunctionCode.WRITE_COIL, "number",
688 new DecimalType("2"), null, bundleContext);
690 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
691 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
692 assertThat(writeRequests.size(), is(equalTo(1)));
693 ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
694 assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL)));
695 assertThat(writeRequest.getReference(), is(equalTo(50)));
696 assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1)));
697 // Since transform output is non-zero, it is mapped as "true"
698 assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(true)));
702 public void testWriteRealTransformation2() throws InvalidSyntaxException {
703 captureModbusWrites();
704 mockTransformation("ZERO", new TransformationService() {
707 public String transform(String function, String source) throws TransformationException {
711 ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "ZERO(foobar)",
712 ModbusConstants.ValueType.BIT, "coil", ModbusWriteFunctionCode.WRITE_COIL, "number",
713 new DecimalType("2"), null, bundleContext);
715 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
716 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
717 assertThat(writeRequests.size(), is(equalTo(1)));
718 ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
719 assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL)));
720 assertThat(writeRequest.getReference(), is(equalTo(50)));
721 assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1)));
722 // Since transform output is zero, it is mapped as "false"
723 assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(false)));
727 public void testWriteRealTransformation3() throws InvalidSyntaxException {
728 captureModbusWrites();
729 mockTransformation("RANDOM", new TransformationService() {
732 public String transform(String function, String source) throws TransformationException {
736 ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "RANDOM(foobar)",
737 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER, "number",
738 new DecimalType("2"), null, bundleContext);
740 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
741 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
742 assertThat(writeRequests.size(), is(equalTo(1)));
743 ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
744 assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER)));
745 assertThat(writeRequest.getReference(), is(equalTo(50)));
746 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1)));
747 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0), is(equalTo(5)));
751 public void testWriteRealTransformation4() throws InvalidSyntaxException {
752 captureModbusWrites();
753 mockTransformation("JSON", new TransformationService() {
756 public String transform(String function, String source) throws TransformationException {
758 + "\"functionCode\": 16,"//
759 + "\"address\": 5412,"//
760 + "\"value\": [1, 0, 5]"//
763 + "\"functionCode\": 6,"//
764 + "\"address\": 555,"//
769 ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "JSON(foobar)",
770 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS, "number",
771 new DecimalType("2"), null, bundleContext);
773 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
774 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
775 assertThat(writeRequests.size(), is(equalTo(2)));
777 ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
778 assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS)));
779 assertThat(writeRequest.getReference(), is(equalTo(5412)));
780 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(3)));
781 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0),
783 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(1),
785 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(2),
789 ModbusWriteRequestBlueprint writeRequest = writeRequests.get(1);
790 assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER)));
791 assertThat(writeRequest.getReference(), is(equalTo(555)));
792 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1)));
793 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0),
799 public void testWriteRealTransformation5() throws InvalidSyntaxException {
800 captureModbusWrites();
801 mockTransformation("PLUS", new TransformationService() {
804 public String transform(String arg, String source) throws TransformationException {
805 return String.valueOf(Integer.parseInt(arg) + Integer.parseInt(source));
808 mockTransformation("CONCAT", new TransformationService() {
811 public String transform(String function, String source) throws TransformationException {
812 return source + function;
815 mockTransformation("MULTIPLY", new MultiplyTransformation());
816 ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "MULTIPLY:3∩PLUS(2)∩CONCAT(0)",
817 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER, "number",
818 new DecimalType("2"), null, bundleContext);
820 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
821 assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
822 assertThat(writeRequests.size(), is(equalTo(1)));
823 ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
824 assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER)));
825 assertThat(writeRequest.getReference(), is(equalTo(50)));
826 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1)));
827 assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0),
828 is(equalTo(/* (2*3 + 2) + '0' */ 80)));
831 private void testValueTypeGeneric(ModbusReadFunctionCode functionCode, ValueType valueType,
832 ThingStatus expectedStatus) {
833 ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
835 // Minimally mocked request
836 ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
837 doReturn(3).when(request).getDataLength();
838 doReturn(functionCode).when(request).getFunctionCode();
840 PollTask task = Mockito.mock(PollTask.class);
841 doReturn(endpoint).when(task).getEndpoint();
842 doReturn(request).when(task).getRequest();
844 Bridge poller = createPollerMock("poller1", task);
846 Configuration dataConfig = new Configuration();
847 dataConfig.put("readStart", "1");
848 dataConfig.put("readTransform", "default");
849 dataConfig.put("readValueType", valueType.getConfigValue());
850 ModbusDataThingHandler dataHandler = createDataHandler("data1", poller,
851 builder -> builder.withConfiguration(dataConfig));
852 assertThat(dataHandler.getThing().getStatus(), is(equalTo(expectedStatus)));
856 public void testCoilDoesNotAcceptFloat32ValueType() {
857 testValueTypeGeneric(ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.FLOAT32, ThingStatus.OFFLINE);
861 public void testCoilAcceptsBitValueType() {
862 testValueTypeGeneric(ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
866 public void testDiscreteInputDoesNotAcceptFloat32ValueType() {
867 testValueTypeGeneric(ModbusReadFunctionCode.READ_INPUT_DISCRETES, ModbusConstants.ValueType.FLOAT32,
868 ThingStatus.OFFLINE);
872 public void testDiscreteInputAcceptsBitValueType() {
873 testValueTypeGeneric(ModbusReadFunctionCode.READ_INPUT_DISCRETES, ModbusConstants.ValueType.BIT,
878 public void testRefreshOnData() throws InterruptedException {
879 ModbusReadFunctionCode functionCode = ModbusReadFunctionCode.READ_COILS;
881 ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
885 // Minimally mocked request
886 ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
887 doReturn(pollLength).when(request).getDataLength();
888 doReturn(functionCode).when(request).getFunctionCode();
890 PollTask task = Mockito.mock(PollTask.class);
891 doReturn(endpoint).when(task).getEndpoint();
892 doReturn(request).when(task).getRequest();
894 Bridge poller = createPollerMock("poller1", task);
896 Configuration dataConfig = new Configuration();
897 dataConfig.put("readStart", "0");
898 dataConfig.put("readTransform", "default");
899 dataConfig.put("readValueType", "bit");
901 String thingId = "read1";
903 ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
904 builder -> builder.withConfiguration(dataConfig), bundleContext);
905 assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
907 verify(comms, never()).submitOneTimePoll(eq(request), notNull(), notNull());
908 // Wait for all channels to receive the REFRESH command (initiated by the core)
910 () -> verify((ModbusPollerThingHandler) poller.getHandler(), times(CHANNEL_TO_ACCEPTED_TYPE.size()))
913 reset(poller.getHandler());
915 // Issue REFRESH command and verify the results
916 dataHandler.handleCommand(Mockito.mock(ChannelUID.class), RefreshType.REFRESH);
918 // data handler asynchronously calls the poller.refresh() -- it might take some time
919 // We check that refresh is finally called
920 waitForAssert(() -> verify((ModbusPollerThingHandler) poller.getHandler()).refresh());
923 private static Stream<Arguments> provideArgsForUpdateThenCommandFromItem()
928 Arguments.of((short) 0b1011_0100_0000_1111, "1", (short) 0b1011_0100_0000_1101, OnOffType.OFF),
929 Arguments.of((short) 0b1011_0100_0000_1111, "4", (short) 0b1011_0100_0001_1111, OnOffType.ON),
930 // OPEN/CLOSED commands
931 Arguments.of((short) 0b1011_0100_0000_1111, "1", (short) 0b1011_0100_0000_1101, OpenClosedType.CLOSED),
932 Arguments.of((short) 0b1011_0100_0000_1111, "4", (short) 0b1011_0100_0001_1111, OpenClosedType.OPEN),
933 // DecimalType commands
934 Arguments.of((short) 0b1011_0100_0000_1111, "1", (short) 0b1011_0100_0000_1101, new DecimalType(0)),
935 Arguments.of((short) 0b1011_0100_0010_1111, "5", (short) 0b1011_0100_0000_1111, new DecimalType(0)),
936 Arguments.of((short) 0b1011_0100_0000_1111, "4", (short) 0b1011_0100_0001_1111, new DecimalType(5)),
937 Arguments.of((short) 0b1011_0100_0000_1111, "15", (short) 0b0011_0100_0000_1111, new DecimalType(0))
940 // parametrize by channel (yes, it does not matter what channel is used, commands are interpreted all the
942 Stream<String> channels = Stream.of("switch", "number", "contact");
943 return channels.map(channel -> appendArg(a, channel));
948 @MethodSource("provideArgsForUpdateThenCommandFromItem")
949 public void testUpdateFromHandlerThenCommandFromItem(short stateUpdateFromHandler, String bitIndex,
950 short expectedWriteDataToSlave, Command commandFromItem, String channel) {
951 int expectedWriteDataToSlaveUnsigned = expectedWriteDataToSlave & 0xFFFF;
952 captureModbusWrites();
953 Configuration pollerConfig = new Configuration();
954 pollerConfig.put("refresh", 0L); // 0 -> non polling
955 pollerConfig.put("start", 2);
956 pollerConfig.put("length", 3);
957 pollerConfig.put("type", ModbusBindingConstantsInternal.READ_TYPE_HOLDING_REGISTER);
958 ThingUID pollerUID = new ThingUID(ModbusBindingConstantsInternal.THING_TYPE_MODBUS_POLLER, "realPoller");
959 Bridge poller = BridgeBuilder.create(ModbusBindingConstantsInternal.THING_TYPE_MODBUS_POLLER, pollerUID)
960 .withLabel("label for realPoller").withConfiguration(pollerConfig)
961 .withBridge(realEndpointWithMockedComms.getUID()).build();
963 assertEquals(ThingStatus.ONLINE, poller.getStatus(), poller.getStatusInfo().getDescription());
965 Configuration dataConfig = new Configuration();
966 dataConfig.put("writeStart", "3." + bitIndex);
967 dataConfig.put("writeValueType", "bit");
968 dataConfig.put("writeType", "holding");
970 String thingId = "read1";
972 ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
973 builder -> builder.withConfiguration(dataConfig), bundleContext);
974 assertEquals(ThingStatus.ONLINE, dataHandler.getThing().getStatus());
975 assertEquals(pollerUID, dataHandler.getThing().getBridgeUID());
977 AsyncModbusReadResult result = new AsyncModbusReadResult(Mockito.mock(ModbusReadRequestBlueprint.class),
978 new ModbusRegisterArray(/* register 2, dummy data */0, /* register 3 */ stateUpdateFromHandler,
979 /* register 4, dummy data */9));
981 // poller receives some data (and therefore data as well)
982 getPollerCallback(((ModbusPollerThingHandler) poller.getHandler())).handle(result);
983 dataHandler.handleCommand(new ChannelUID(dataHandler.getThing().getUID(), channel), commandFromItem);
985 // Assert data written
987 assertEquals(1, writeRequests.size());
988 ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
989 assertEquals(writeRequest.getFunctionCode(), ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER);
990 assertEquals(writeRequest.getReference(), 3);
991 assertEquals(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), 1);
992 assertEquals(expectedWriteDataToSlaveUnsigned,
993 ((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0));
997 private void testInitGeneric(ModbusReadFunctionCode pollerFunctionCode, Configuration config,
998 Consumer<ThingStatusInfo> statusConsumer) {
999 testInitGeneric(pollerFunctionCode, 0, config, statusConsumer);
1004 * @param pollerFunctionCode poller function code. Use null if you want to have data thing direct child of endpoint
1006 * @param pollerStart start index of poller
1007 * @param config thing config
1008 * @param statusConsumer assertion method for data thingstatus
1010 private void testInitGeneric(ModbusReadFunctionCode pollerFunctionCode, int pollerStart, Configuration config,
1011 Consumer<ThingStatusInfo> statusConsumer) {
1015 if (pollerFunctionCode == null) {
1016 parent = createTcpMock();
1019 ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
1021 // Minimally mocked request
1022 ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
1023 doReturn(pollerStart).when(request).getReference();
1024 doReturn(pollLength).when(request).getDataLength();
1025 doReturn(pollerFunctionCode).when(request).getFunctionCode();
1027 PollTask task = Mockito.mock(PollTask.class);
1028 doReturn(endpoint).when(task).getEndpoint();
1029 doReturn(request).when(task).getRequest();
1031 parent = createPollerMock("poller1", task);
1034 String thingId = "read1";
1036 ModbusDataThingHandler dataHandler = createDataHandler(thingId, parent,
1037 builder -> builder.withConfiguration(config), bundleContext);
1039 statusConsumer.accept(dataHandler.getThing().getStatusInfo());
1043 public void testReadOnlyData() {
1044 Configuration dataConfig = new Configuration();
1045 dataConfig.put("readStart", "0");
1046 dataConfig.put("readValueType", "bit");
1047 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
1048 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1052 * readValueType=bit should be assumed with coils, so it's ok to skip it
1055 public void testReadOnlyDataMissingValueTypeWithCoils() {
1056 Configuration dataConfig = new Configuration();
1057 dataConfig.put("readStart", "0");
1058 // missing value type
1059 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
1060 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1064 public void testReadOnlyDataInvalidValueType() {
1065 Configuration dataConfig = new Configuration();
1066 dataConfig.put("readStart", "0");
1067 dataConfig.put("readValueType", "foobar");
1068 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1069 assertThat(status.getStatus(), is(equalTo(ThingStatus.UNINITIALIZED)));
1070 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)));
1075 * We do not assume value type with registers, not ok to skip it
1078 public void testReadOnlyDataMissingValueTypeWithRegisters() {
1079 Configuration dataConfig = new Configuration();
1080 dataConfig.put("readStart", "0");
1081 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1082 assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1083 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1088 public void testWriteOnlyData() {
1089 Configuration dataConfig = new Configuration();
1090 dataConfig.put("writeStart", "0");
1091 dataConfig.put("writeValueType", "bit");
1092 dataConfig.put("writeType", "coil");
1093 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
1094 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1098 public void testWriteHoldingInt16Data() {
1099 Configuration dataConfig = new Configuration();
1100 dataConfig.put("writeStart", "0");
1101 dataConfig.put("writeValueType", "int16");
1102 dataConfig.put("writeType", "holding");
1103 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
1104 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1108 public void testWriteHoldingInt8Data() {
1109 Configuration dataConfig = new Configuration();
1110 dataConfig.put("writeStart", "0");
1111 dataConfig.put("writeValueType", "int8");
1112 dataConfig.put("writeType", "holding");
1113 testInitGeneric(null, dataConfig, status -> {
1114 assertThat(status.getStatus(), is(equalTo(ThingStatus.UNINITIALIZED)));
1115 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)));
1120 public void testWriteHoldingBitDataWrongWriteType() {
1121 Configuration dataConfig = new Configuration();
1122 dataConfig.put("writeStart", "0.15");
1123 dataConfig.put("writeValueType", "bit");
1124 dataConfig.put("writeType", "coil"); // X.Y writeStart only applicable with holding
1125 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1126 assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
1127 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1132 public void testWriteHoldingBitData() {
1133 Configuration dataConfig = new Configuration();
1134 dataConfig.put("writeStart", "0.15");
1135 dataConfig.put("writeValueType", "bit");
1136 dataConfig.put("writeType", "holding");
1137 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1138 assertEquals(status.getStatus(), ThingStatus.ONLINE, status.getDescription());
1143 public void testWriteHoldingInt8WithSubIndexData() {
1144 Configuration dataConfig = new Configuration();
1145 dataConfig.put("writeStart", "1.0");
1146 dataConfig.put("writeValueType", "int8");
1147 dataConfig.put("writeType", "holding");
1148 // OFFLINE since sub-register writes are not supported for other than bit
1149 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1150 assertThat(status.getStatus(), is(equalTo(ThingStatus.UNINITIALIZED)));
1151 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)));
1156 public void testWriteHoldingBitDataRegisterOutOfBounds() {
1157 Configuration dataConfig = new Configuration();
1158 // in this test poller reads from register 2. Register 1 is out of bounds
1159 dataConfig.put("writeStart", "1.15");
1160 dataConfig.put("writeValueType", "bit");
1161 dataConfig.put("writeType", "holding");
1162 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, /* poller start */2, dataConfig, status -> {
1163 assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
1164 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1169 public void testWriteHoldingBitDataRegisterOutOfBounds2() {
1170 Configuration dataConfig = new Configuration();
1171 // register 3 is the last one polled, 4 is out of bounds
1172 dataConfig.put("writeStart", "4.15");
1173 dataConfig.put("writeValueType", "bit");
1174 dataConfig.put("writeType", "holding");
1175 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1176 assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
1181 @CsvSource({ "READ_COILS", "READ_INPUT_DISCRETES", "READ_INPUT_REGISTERS" })
1182 public void testWriteHoldingBitDataWrongPoller(ModbusReadFunctionCode poller) {
1183 Configuration dataConfig = new Configuration();
1184 dataConfig.put("writeStart", "0.15");
1185 dataConfig.put("writeValueType", "bit");
1186 dataConfig.put("writeType", "holding");
1187 testInitGeneric(poller, dataConfig, status -> {
1188 assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
1189 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1194 public void testWriteHoldingBitParentEndpointData() {
1195 Configuration dataConfig = new Configuration();
1196 dataConfig.put("writeStart", "0.15");
1197 dataConfig.put("writeValueType", "bit");
1198 dataConfig.put("writeType", "holding");
1199 // OFFLINE since we require poller as parent when sub-register writes are used
1200 testInitGeneric(/* poller not as parent */null, dataConfig, status -> {
1201 assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
1202 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1207 public void testWriteHoldingBitBadStartData() {
1208 Configuration dataConfig = new Configuration();
1209 dataConfig.put("writeStart", "0.16");
1210 dataConfig.put("writeValueType", "int8");
1211 dataConfig.put("writeType", "holding");
1212 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1213 assertThat(status.getStatus(), is(equalTo(ThingStatus.UNINITIALIZED)));
1214 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)));
1219 public void testWriteOnlyDataChildOfEndpoint() {
1220 Configuration dataConfig = new Configuration();
1221 dataConfig.put("writeStart", "0");
1222 dataConfig.put("writeValueType", "bit");
1223 dataConfig.put("writeType", "coil");
1224 testInitGeneric(null, dataConfig, status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1228 public void testWriteOnlyDataMissingOneParameter() {
1229 Configuration dataConfig = new Configuration();
1230 dataConfig.put("writeStart", "0");
1231 dataConfig.put("writeValueType", "bit");
1232 // missing writeType --> error
1233 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1234 assertEquals(ThingStatus.OFFLINE, status.getStatus(), status.getDescription());
1235 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1236 assertThat(status.getDescription(), is(not(equalTo(null))));
1241 * OK to omit writeValueType with coils since bit is assumed
1244 public void testWriteOnlyDataMissingValueTypeWithCoilParameter() {
1245 Configuration dataConfig = new Configuration();
1246 dataConfig.put("writeStart", "0");
1247 dataConfig.put("writeType", "coil");
1248 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
1249 status -> assertEquals(ThingStatus.ONLINE, status.getStatus(), status.getDescription()));
1253 public void testWriteOnlyIllegalValueType() {
1254 Configuration dataConfig = new Configuration();
1255 dataConfig.put("writeStart", "0");
1256 dataConfig.put("writeType", "coil");
1257 dataConfig.put("writeValueType", "foobar");
1258 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1259 assertThat(status.getStatus(), is(equalTo(ThingStatus.UNINITIALIZED)));
1260 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)));
1265 public void testWriteInvalidType() {
1266 Configuration dataConfig = new Configuration();
1267 dataConfig.put("writeStart", "0");
1268 dataConfig.put("writeType", "foobar");
1269 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1270 assertThat(status.getStatus(), is(equalTo(ThingStatus.UNINITIALIZED)));
1271 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)));
1276 public void testWriteCoilBadStart() {
1277 Configuration dataConfig = new Configuration();
1278 dataConfig.put("writeStart", "0.4");
1279 dataConfig.put("writeType", "coil");
1280 testInitGeneric(null, dataConfig, status -> {
1281 assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1282 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1287 public void testWriteHoldingBadStart() {
1288 Configuration dataConfig = new Configuration();
1289 dataConfig.put("writeStart", "0.4");
1290 dataConfig.put("writeType", "holding");
1291 testInitGeneric(null, dataConfig, status -> {
1292 assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1293 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1298 public void testReadHoldingBadStart() {
1299 Configuration dataConfig = new Configuration();
1300 dataConfig.put("readStart", "0.0");
1301 dataConfig.put("readValueType", "int16");
1302 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1303 assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1304 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1309 public void testReadHoldingBadStart2() {
1310 Configuration dataConfig = new Configuration();
1311 dataConfig.put("readStart", "0.0");
1312 dataConfig.put("readValueType", "bit");
1313 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1314 assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1315 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1320 public void testReadHoldingOKStart() {
1321 Configuration dataConfig = new Configuration();
1322 dataConfig.put("readStart", "0.0");
1323 dataConfig.put("readType", "holding");
1324 dataConfig.put("readValueType", "bit");
1325 testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig,
1326 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1330 public void testReadValueTypeIllegal() {
1331 Configuration dataConfig = new Configuration();
1332 dataConfig.put("readStart", "0.0");
1333 dataConfig.put("readType", "holding");
1334 dataConfig.put("readValueType", "foobar");
1335 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1336 assertThat(status.getStatus(), is(equalTo(ThingStatus.UNINITIALIZED)));
1337 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.HANDLER_CONFIGURATION_PENDING)));
1342 public void testWriteOnlyTransform() {
1343 Configuration dataConfig = new Configuration();
1344 // no need to have start, JSON output of transformation defines everything
1345 dataConfig.put("writeTransform", "JS(myJsonTransform.js)");
1346 testInitGeneric(null, dataConfig, status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1350 public void testWriteTransformAndStart() {
1351 Configuration dataConfig = new Configuration();
1352 // It's illegal to have start and transform. Just have transform or have all
1353 dataConfig.put("writeStart", "3");
1354 dataConfig.put("writeTransform", "JS(myJsonTransform.js)");
1355 testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1356 assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1357 assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1362 public void testWriteTransformAndNecessary() {
1363 Configuration dataConfig = new Configuration();
1364 dataConfig.put("writeStart", "3");
1365 dataConfig.put("writeType", "holding");
1366 dataConfig.put("writeValueType", "int16");
1367 dataConfig.put("writeTransform", "JS(myJsonTransform.js)");
1368 testInitGeneric(null, dataConfig, status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));