]> git.basschouten.com Git - openhab-addons.git/blob
a44eeaa206f0304ec86d147d3295e84505754185
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.modbus.tests;
14
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.*;
22
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;
28 import java.util.Map;
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;
35
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;
93
94 /**
95  * @author Sami Salonen - Initial contribution
96  */
97 public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
98
99     private final class MultiplyTransformation implements TransformationService {
100         @Override
101         public String transform(String function, String source) throws TransformationException {
102             return String.valueOf(Integer.parseInt(function) * Integer.parseInt(source));
103         }
104     }
105
106     private static final String HOST = "thisishost";
107     private static final int PORT = 44;
108
109     private static final Map<String, String> CHANNEL_TO_ACCEPTED_TYPE = new HashMap<>();
110     static {
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");
122     }
123     private List<ModbusWriteRequestBlueprint> writeRequests = new ArrayList<>();
124     private Bridge realEndpointWithMockedComms;
125
126     public ModbusReadCallback getPollerCallback(ModbusPollerThingHandler handler) {
127         Field callbackField;
128         try {
129             callbackField = ModbusPollerThingHandler.class.getDeclaredField("callbackDelegator");
130             callbackField.setAccessible(true);
131             return (ModbusReadCallback) callbackField.get(handler);
132         } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
133             fail(e);
134             throw new RuntimeException(e);
135         }
136     }
137
138     @BeforeEach
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);
145
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());
153     }
154
155     @AfterEach
156     public void tearDown() {
157         writeRequests.clear();
158         if (realEndpointWithMockedComms != null) {
159             thingProvider.remove(realEndpointWithMockedComms.getUID());
160         }
161     }
162
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);
167     }
168
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);
174         });
175     }
176
177     private Bridge createPollerMock(String pollerId, PollTask task) {
178         final Bridge poller;
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());
187         }
188         poller = builder.build();
189         poller.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
190
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(),
199                 task.getEndpoint());
200         assertSame(((ModbusPollerThingHandler) poller.getHandler()).getRequest(), task.getRequest());
201
202         addThing(poller);
203         return poller;
204     }
205
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();
212         try {
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();
217         }
218         tcpThingHandler.initialize();
219         assertThat(tcpBridge.getStatus(), is(equalTo(ThingStatus.ONLINE)));
220         return tcpBridge;
221     }
222
223     private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
224             Function<ThingBuilder, ThingBuilder> builderConfigurator) {
225         return createDataHandler(id, bridge, builderConfigurator, null);
226     }
227
228     private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
229             Function<ThingBuilder, ThingBuilder> builderConfigurator, BundleContext context) {
230         return createDataHandler(id, bridge, builderConfigurator, context, true);
231     }
232
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());
245
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,
252                         is(notNullValue()));
253                 assertNotNull(item);
254                 Objects.requireNonNull(item);
255                 addItem(item);
256                 toBeLinked.put(itemName, channelUID);
257             }
258         }
259         if (builderConfigurator != null) {
260             builder = builderConfigurator.apply(builder);
261         }
262
263         Thing dataThing = builder.withBridge(bridge.getUID()).build();
264         addThing(dataThing);
265
266         // Link after the things and items have been created
267         for (Entry<String, ChannelUID> entry : toBeLinked.entrySet()) {
268             linkItem(entry.getKey(), entry.getValue());
269         }
270         return (ModbusDataThingHandler) dataThing.getHandler();
271     }
272
273     private String getItemName(ChannelUID channelUID) {
274         return channelUID.toString().replace(':', '_') + "_item";
275     }
276
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,
283                     is(notNullValue()));
284             assertNotNull(item);
285             List<State> updates = getStateUpdates(itemName);
286             if (updates != null) {
287                 assertThat(
288                         String.format("Many updates found, expected one: %s", Arrays.deepToString(updates.toArray())),
289                         updates.size(), is(equalTo(1)));
290             }
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));
294         });
295     }
296
297     private void assertSingleStateUpdate(ModbusDataThingHandler handler, String channel, State state) {
298         assertSingleStateUpdate(handler, channel, is(equalTo(state)));
299     }
300
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);
304     }
305
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);
310
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();
316
317         PollTask task = Mockito.mock(PollTask.class);
318         doReturn(endpoint).when(task).getEndpoint();
319         doReturn(request).when(task).getRequest();
320
321         Bridge pollerThing = createPollerMock("poller1", task);
322
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)));
331     }
332
333     @Test
334     public void testInitCoilsOutOfIndex() {
335         testOutOfBoundsGeneric(4, 3, "8", ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT,
336                 ThingStatus.OFFLINE);
337     }
338
339     @Test
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);
344     }
345
346     @Test
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,
350                 ThingStatus.ONLINE);
351     }
352
353     @Test
354     public void testInitRegistersWithBitOutOfIndex() {
355         testOutOfBoundsGeneric(4, 3, "8.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
356                 ModbusConstants.ValueType.BIT, ThingStatus.OFFLINE);
357     }
358
359     @Test
360     public void testInitRegistersWithBitOutOfIndex2() {
361         testOutOfBoundsGeneric(4, 3, "7.16", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
362                 ModbusConstants.ValueType.BIT, ThingStatus.OFFLINE);
363     }
364
365     @Test
366     public void testInitRegistersWithBitOK() {
367         testOutOfBoundsGeneric(4, 3, "6.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
368                 ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
369     }
370
371     @Test
372     public void testInitRegistersWithBitOK2() {
373         testOutOfBoundsGeneric(4, 3, "6.15", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
374                 ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
375     }
376
377     @Test
378     public void testInitRegistersWithInt8OutOfIndex() {
379         testOutOfBoundsGeneric(4, 3, "8.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
380                 ModbusConstants.ValueType.INT8, ThingStatus.OFFLINE);
381     }
382
383     @Test
384     public void testInitRegistersWithInt8OutOfIndex2() {
385         testOutOfBoundsGeneric(4, 3, "7.2", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
386                 ModbusConstants.ValueType.INT8, ThingStatus.OFFLINE);
387     }
388
389     @Test
390     public void testInitRegistersWithInt8OK() {
391         testOutOfBoundsGeneric(4, 3, "6.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
392                 ModbusConstants.ValueType.INT8, ThingStatus.ONLINE);
393     }
394
395     @Test
396     public void testInitRegistersWithInt8OK2() {
397         testOutOfBoundsGeneric(4, 3, "6.1", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
398                 ModbusConstants.ValueType.INT8, ThingStatus.ONLINE);
399     }
400
401     @Test
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);
406     }
407
408     @Test
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);
413     }
414
415     @Test
416     public void testInitRegistersWithInt16OutOfBounds2() {
417         testOutOfBoundsGeneric(4, 3, "8", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
418                 ModbusConstants.ValueType.INT16, ThingStatus.OFFLINE);
419     }
420
421     @Test
422     public void testInitRegistersWithInt16NoDecimalFormatAllowed() {
423         testOutOfBoundsGeneric(4, 3, "7.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
424                 ModbusConstants.ValueType.INT16, ThingStatus.OFFLINE);
425     }
426
427     @Test
428     public void testInitRegistersWithInt32OK() {
429         testOutOfBoundsGeneric(4, 3, "5", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
430                 ModbusConstants.ValueType.INT32, ThingStatus.ONLINE);
431     }
432
433     @Test
434     public void testInitRegistersWithInt32OutOfBounds() {
435         testOutOfBoundsGeneric(4, 3, "6", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
436                 ModbusConstants.ValueType.INT32, ThingStatus.OFFLINE);
437     }
438
439     @Test
440     public void testInitRegistersWithInt32AtTheEdge() {
441         testOutOfBoundsGeneric(4, 3, "5", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
442                 ModbusConstants.ValueType.INT32, ThingStatus.ONLINE);
443     }
444
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);
448     }
449
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,
454                 true);
455     }
456
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);
462
463         int pollLength = 3;
464
465         // Minimally mocked request
466         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
467         doReturn(pollLength).when(request).getDataLength();
468         doReturn(functionCode).when(request).getFunctionCode();
469
470         PollTask task = Mockito.mock(PollTask.class);
471         doReturn(endpoint).when(task).getEndpoint();
472         doReturn(request).when(task).getRequest();
473
474         Bridge poller = createPollerMock("poller1", task);
475
476         Configuration dataConfig = new Configuration();
477         dataConfig.put("readStart", start);
478         dataConfig.put("readTransform", transform);
479         dataConfig.put("readValueType", valueType.getConfigValue());
480
481         String thingId = "read1";
482         ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
483                 builder -> builder.withConfiguration(dataConfig), context, autoCreateItemsAndLinkToChannels);
484
485         assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
486
487         // call callbacks
488         if (bits != null) {
489             assertNull(registers);
490             assertNull(error);
491             AsyncModbusReadResult result = new AsyncModbusReadResult(request, bits);
492             dataHandler.onReadResult(result);
493         } else if (registers != null) {
494             assertNull(bits);
495             assertNull(error);
496             AsyncModbusReadResult result = new AsyncModbusReadResult(request, registers);
497             dataHandler.onReadResult(result);
498         } else {
499             assertNull(bits);
500             assertNull(registers);
501             assertNotNull(error);
502             AsyncModbusFailure<ModbusReadRequestBlueprint> result = new AsyncModbusFailure<ModbusReadRequestBlueprint>(
503                     request, error);
504             dataHandler.handleReadError(result);
505         }
506         return dataHandler;
507     }
508
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,
513                 context, false);
514     }
515
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);
521
522         // Minimally mocked request
523         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
524
525         PollTask task = Mockito.mock(PollTask.class);
526         doReturn(endpoint).when(task).getEndpoint();
527         doReturn(request).when(task).getRequest();
528
529         final Bridge parent;
530         if (parentIsEndpoint) {
531             parent = createTcpMock();
532             addThing(parent);
533         } else {
534             parent = createPollerMock("poller1", task);
535         }
536
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);
543
544         String thingId = "write";
545
546         ModbusDataThingHandler dataHandler = createDataHandler(thingId, parent,
547                 builder -> builder.withConfiguration(dataConfig), context);
548
549         assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
550
551         dataHandler.handleCommand(new ChannelUID(dataHandler.getThing().getUID(), channel), command);
552
553         if (error != null) {
554             dataHandler.handleReadError(new AsyncModbusFailure<ModbusReadRequestBlueprint>(request, error));
555         } else {
556             ModbusResponse resp = new ModbusResponse() {
557
558                 @Override
559                 public int getFunctionCode() {
560                     return successFC.getFunctionCode();
561                 }
562             };
563             dataHandler
564                     .onWriteResponse(new AsyncModbusWriteResult(Mockito.mock(ModbusWriteRequestBlueprint.class), resp));
565         }
566         return dataHandler;
567     }
568
569     @Test
570     public void testOnError() {
571         ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
572                 "0.0", "default", ModbusConstants.ValueType.BIT, null, null, new Exception("fooerror"));
573
574         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(notNullValue(State.class)));
575     }
576
577     @Test
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);
582
583         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
584         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
585
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
595     }
596
597     @Test
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);
603
604         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
605         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
606
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
617     }
618
619     @Test
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);
626
627         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
628         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
629
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);
637     }
638
639     @Test
640     public void testOnRegistersRealTransformation2() throws InvalidSyntaxException {
641         mockTransformation("ONOFF", new TransformationService() {
642
643             @Override
644             public String transform(String function, String source) throws TransformationException {
645                 return Integer.parseInt(source) != 0 ? "ON" : "OFF";
646             }
647         });
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);
651
652         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
653         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
654
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"))));
661     }
662
663     @Test
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);
670
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)));
680     }
681
682     @Test
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);
689
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)));
699     }
700
701     @Test
702     public void testWriteRealTransformation2() throws InvalidSyntaxException {
703         captureModbusWrites();
704         mockTransformation("ZERO", new TransformationService() {
705
706             @Override
707             public String transform(String function, String source) throws TransformationException {
708                 return "0";
709             }
710         });
711         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "ZERO(foobar)",
712                 ModbusConstants.ValueType.BIT, "coil", ModbusWriteFunctionCode.WRITE_COIL, "number",
713                 new DecimalType("2"), null, bundleContext);
714
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)));
724     }
725
726     @Test
727     public void testWriteRealTransformation3() throws InvalidSyntaxException {
728         captureModbusWrites();
729         mockTransformation("RANDOM", new TransformationService() {
730
731             @Override
732             public String transform(String function, String source) throws TransformationException {
733                 return "5";
734             }
735         });
736         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "RANDOM(foobar)",
737                 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER, "number",
738                 new DecimalType("2"), null, bundleContext);
739
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)));
748     }
749
750     @Test
751     public void testWriteRealTransformation4() throws InvalidSyntaxException {
752         captureModbusWrites();
753         mockTransformation("JSON", new TransformationService() {
754
755             @Override
756             public String transform(String function, String source) throws TransformationException {
757                 return "[{"//
758                         + "\"functionCode\": 16,"//
759                         + "\"address\": 5412,"//
760                         + "\"value\": [1, 0, 5]"//
761                         + "},"//
762                         + "{"//
763                         + "\"functionCode\": 6,"//
764                         + "\"address\": 555,"//
765                         + "\"value\": [3]"//
766                         + "}]";
767             }
768         });
769         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "JSON(foobar)",
770                 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS, "number",
771                 new DecimalType("2"), null, bundleContext);
772
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)));
776         {
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),
782                     is(equalTo(1)));
783             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(1),
784                     is(equalTo(0)));
785             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(2),
786                     is(equalTo(5)));
787         }
788         {
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),
794                     is(equalTo(3)));
795         }
796     }
797
798     @Test
799     public void testWriteRealTransformation5() throws InvalidSyntaxException {
800         captureModbusWrites();
801         mockTransformation("PLUS", new TransformationService() {
802
803             @Override
804             public String transform(String arg, String source) throws TransformationException {
805                 return String.valueOf(Integer.parseInt(arg) + Integer.parseInt(source));
806             }
807         });
808         mockTransformation("CONCAT", new TransformationService() {
809
810             @Override
811             public String transform(String function, String source) throws TransformationException {
812                 return source + function;
813             }
814         });
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);
819
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)));
829     }
830
831     private void testValueTypeGeneric(ModbusReadFunctionCode functionCode, ValueType valueType,
832             ThingStatus expectedStatus) {
833         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
834
835         // Minimally mocked request
836         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
837         doReturn(3).when(request).getDataLength();
838         doReturn(functionCode).when(request).getFunctionCode();
839
840         PollTask task = Mockito.mock(PollTask.class);
841         doReturn(endpoint).when(task).getEndpoint();
842         doReturn(request).when(task).getRequest();
843
844         Bridge poller = createPollerMock("poller1", task);
845
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)));
853     }
854
855     @Test
856     public void testCoilDoesNotAcceptFloat32ValueType() {
857         testValueTypeGeneric(ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.FLOAT32, ThingStatus.OFFLINE);
858     }
859
860     @Test
861     public void testCoilAcceptsBitValueType() {
862         testValueTypeGeneric(ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
863     }
864
865     @Test
866     public void testDiscreteInputDoesNotAcceptFloat32ValueType() {
867         testValueTypeGeneric(ModbusReadFunctionCode.READ_INPUT_DISCRETES, ModbusConstants.ValueType.FLOAT32,
868                 ThingStatus.OFFLINE);
869     }
870
871     @Test
872     public void testDiscreteInputAcceptsBitValueType() {
873         testValueTypeGeneric(ModbusReadFunctionCode.READ_INPUT_DISCRETES, ModbusConstants.ValueType.BIT,
874                 ThingStatus.ONLINE);
875     }
876
877     @Test
878     public void testRefreshOnData() throws InterruptedException {
879         ModbusReadFunctionCode functionCode = ModbusReadFunctionCode.READ_COILS;
880
881         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
882
883         int pollLength = 3;
884
885         // Minimally mocked request
886         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
887         doReturn(pollLength).when(request).getDataLength();
888         doReturn(functionCode).when(request).getFunctionCode();
889
890         PollTask task = Mockito.mock(PollTask.class);
891         doReturn(endpoint).when(task).getEndpoint();
892         doReturn(request).when(task).getRequest();
893
894         Bridge poller = createPollerMock("poller1", task);
895
896         Configuration dataConfig = new Configuration();
897         dataConfig.put("readStart", "0");
898         dataConfig.put("readTransform", "default");
899         dataConfig.put("readValueType", "bit");
900
901         String thingId = "read1";
902
903         ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
904                 builder -> builder.withConfiguration(dataConfig), bundleContext);
905         assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
906
907         verify(comms, never()).submitOneTimePoll(eq(request), notNull(), notNull());
908         // Wait for all channels to receive the REFRESH command (initiated by the core)
909         waitForAssert(
910                 () -> verify((ModbusPollerThingHandler) poller.getHandler(), times(CHANNEL_TO_ACCEPTED_TYPE.size()))
911                         .refresh());
912         // Reset the mock
913         reset(poller.getHandler());
914
915         // Issue REFRESH command and verify the results
916         dataHandler.handleCommand(Mockito.mock(ChannelUID.class), RefreshType.REFRESH);
917
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());
921     }
922
923     private static Stream<Arguments> provideArgsForUpdateThenCommandFromItem()
924
925     {
926         return Stream.of(//
927                 // ON/OFF commands
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))
938
939         ).flatMap(a -> {
940             // parametrize by channel (yes, it does not matter what channel is used, commands are interpreted all the
941             // same)
942             Stream<String> channels = Stream.of("switch", "number", "contact");
943             return channels.map(channel -> appendArg(a, channel));
944         });
945     }
946
947     @ParameterizedTest
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();
962         addThing(poller);
963         assertEquals(ThingStatus.ONLINE, poller.getStatus(), poller.getStatusInfo().getDescription());
964
965         Configuration dataConfig = new Configuration();
966         dataConfig.put("writeStart", "3." + bitIndex);
967         dataConfig.put("writeValueType", "bit");
968         dataConfig.put("writeType", "holding");
969
970         String thingId = "read1";
971
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());
976
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));
980
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);
984
985         // Assert data written
986         {
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));
994         }
995     }
996
997     private void testInitGeneric(ModbusReadFunctionCode pollerFunctionCode, Configuration config,
998             Consumer<ThingStatusInfo> statusConsumer) {
999         testInitGeneric(pollerFunctionCode, 0, config, statusConsumer);
1000     }
1001
1002     /**
1003      *
1004      * @param pollerFunctionCode poller function code. Use null if you want to have data thing direct child of endpoint
1005      *            thing
1006      * @param pollerStart start index of poller
1007      * @param config thing config
1008      * @param statusConsumer assertion method for data thingstatus
1009      */
1010     private void testInitGeneric(ModbusReadFunctionCode pollerFunctionCode, int pollerStart, Configuration config,
1011             Consumer<ThingStatusInfo> statusConsumer) {
1012         int pollLength = 3;
1013
1014         Bridge parent;
1015         if (pollerFunctionCode == null) {
1016             parent = createTcpMock();
1017             addThing(parent);
1018         } else {
1019             ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
1020
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();
1026
1027             PollTask task = Mockito.mock(PollTask.class);
1028             doReturn(endpoint).when(task).getEndpoint();
1029             doReturn(request).when(task).getRequest();
1030
1031             parent = createPollerMock("poller1", task);
1032         }
1033
1034         String thingId = "read1";
1035
1036         ModbusDataThingHandler dataHandler = createDataHandler(thingId, parent,
1037                 builder -> builder.withConfiguration(config), bundleContext);
1038
1039         statusConsumer.accept(dataHandler.getThing().getStatusInfo());
1040     }
1041
1042     @Test
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))));
1049     }
1050
1051     /**
1052      * readValueType=bit should be assumed with coils, so it's ok to skip it
1053      */
1054     @Test
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))));
1061     }
1062
1063     @Test
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)));
1071         });
1072     }
1073
1074     /**
1075      * We do not assume value type with registers, not ok to skip it
1076      */
1077     @Test
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)));
1084         });
1085     }
1086
1087     @Test
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))));
1095     }
1096
1097     @Test
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))));
1105     }
1106
1107     @Test
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)));
1116         });
1117     }
1118
1119     @Test
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)));
1128         });
1129     }
1130
1131     @Test
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());
1139         });
1140     }
1141
1142     @Test
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)));
1152         });
1153     }
1154
1155     @Test
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)));
1165         });
1166     }
1167
1168     @Test
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());
1177         });
1178     }
1179
1180     @ParameterizedTest
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)));
1190         });
1191     }
1192
1193     @Test
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)));
1203         });
1204     }
1205
1206     @Test
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)));
1215         });
1216     }
1217
1218     @Test
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))));
1225     }
1226
1227     @Test
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))));
1237         });
1238     }
1239
1240     /**
1241      * OK to omit writeValueType with coils since bit is assumed
1242      */
1243     @Test
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()));
1250     }
1251
1252     @Test
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)));
1261         });
1262     }
1263
1264     @Test
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)));
1272         });
1273     }
1274
1275     @Test
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)));
1283         });
1284     }
1285
1286     @Test
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)));
1294         });
1295     }
1296
1297     @Test
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)));
1305         });
1306     }
1307
1308     @Test
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)));
1316         });
1317     }
1318
1319     @Test
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))));
1327     }
1328
1329     @Test
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)));
1338         });
1339     }
1340
1341     @Test
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))));
1347     }
1348
1349     @Test
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)));
1358         });
1359     }
1360
1361     @Test
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))));
1369     }
1370 }