]> git.basschouten.com Git - openhab-addons.git/blob
1f3cf38aa6f67d985851a150a166e006437fbd6c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.Objects;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.function.Consumer;
32 import java.util.function.Function;
33
34 import org.hamcrest.Matcher;
35 import org.junit.jupiter.api.AfterEach;
36 import org.junit.jupiter.api.Test;
37 import org.mockito.Mockito;
38 import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
39 import org.openhab.binding.modbus.handler.ModbusPollerThingHandler;
40 import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler;
41 import org.openhab.binding.modbus.internal.handler.ModbusTcpThingHandler;
42 import org.openhab.core.config.core.Configuration;
43 import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
44 import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
45 import org.openhab.core.io.transport.modbus.AsyncModbusWriteResult;
46 import org.openhab.core.io.transport.modbus.BitArray;
47 import org.openhab.core.io.transport.modbus.ModbusConstants;
48 import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
49 import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
50 import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
51 import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
52 import org.openhab.core.io.transport.modbus.ModbusResponse;
53 import org.openhab.core.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
54 import org.openhab.core.io.transport.modbus.ModbusWriteFunctionCode;
55 import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
56 import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
57 import org.openhab.core.io.transport.modbus.PollTask;
58 import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
59 import org.openhab.core.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
60 import org.openhab.core.items.GenericItem;
61 import org.openhab.core.items.Item;
62 import org.openhab.core.library.types.DecimalType;
63 import org.openhab.core.library.types.OnOffType;
64 import org.openhab.core.library.types.OpenClosedType;
65 import org.openhab.core.library.types.StringType;
66 import org.openhab.core.thing.Bridge;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.ThingStatusInfo;
72 import org.openhab.core.thing.ThingUID;
73 import org.openhab.core.thing.binding.builder.BridgeBuilder;
74 import org.openhab.core.thing.binding.builder.ChannelBuilder;
75 import org.openhab.core.thing.binding.builder.ThingBuilder;
76 import org.openhab.core.transform.TransformationException;
77 import org.openhab.core.transform.TransformationService;
78 import org.openhab.core.types.Command;
79 import org.openhab.core.types.RefreshType;
80 import org.openhab.core.types.State;
81 import org.openhab.core.types.UnDefType;
82 import org.osgi.framework.BundleContext;
83 import org.osgi.framework.InvalidSyntaxException;
84
85 /**
86  * @author Sami Salonen - Initial contribution
87  */
88 public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
89
90     private final class MultiplyTransformation implements TransformationService {
91         @Override
92         public String transform(String function, String source) throws TransformationException {
93             return String.valueOf(Integer.parseInt(function) * Integer.parseInt(source));
94         }
95     }
96
97     private static final Map<String, String> CHANNEL_TO_ACCEPTED_TYPE = new HashMap<>();
98     static {
99         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_SWITCH, "Switch");
100         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_CONTACT, "Contact");
101         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_DATETIME, "DateTime");
102         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_DIMMER, "Dimmer");
103         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_NUMBER, "Number");
104         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_STRING, "String");
105         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_ROLLERSHUTTER, "Rollershutter");
106         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_READ_SUCCESS, "DateTime");
107         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_WRITE_SUCCESS, "DateTime");
108         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_WRITE_ERROR, "DateTime");
109         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_READ_ERROR, "DateTime");
110     }
111     private List<ModbusWriteRequestBlueprint> writeRequests = new ArrayList<>();
112
113     @AfterEach
114     public void tearDown() {
115         writeRequests.clear();
116     }
117
118     private void captureModbusWrites() {
119         Mockito.when(comms.submitOneTimeWrite(any(), any(), any())).then(invocation -> {
120             ModbusWriteRequestBlueprint task = (ModbusWriteRequestBlueprint) invocation.getArgument(0);
121             writeRequests.add(task);
122             return Mockito.mock(ScheduledFuture.class);
123         });
124     }
125
126     private Bridge createPollerMock(String pollerId, PollTask task) {
127         final Bridge poller;
128         ThingUID thingUID = new ThingUID(THING_TYPE_MODBUS_POLLER, pollerId);
129         BridgeBuilder builder = BridgeBuilder.create(THING_TYPE_MODBUS_POLLER, thingUID)
130                 .withLabel("label for " + pollerId);
131         for (Entry<String, String> entry : CHANNEL_TO_ACCEPTED_TYPE.entrySet()) {
132             String channelId = entry.getKey();
133             String channelAcceptedType = entry.getValue();
134             builder = builder.withChannel(
135                     ChannelBuilder.create(new ChannelUID(thingUID, channelId), channelAcceptedType).build());
136         }
137         poller = builder.build();
138         poller.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
139
140         ModbusPollerThingHandler mockHandler = Mockito.mock(ModbusPollerThingHandler.class);
141         doReturn(task.getRequest()).when(mockHandler).getRequest();
142         assert comms != null;
143         doReturn(comms).when(mockHandler).getCommunicationInterface();
144         doReturn(task.getEndpoint()).when(comms).getEndpoint();
145         poller.setHandler(mockHandler);
146         assertSame(poller.getHandler(), mockHandler);
147         assertSame(((ModbusPollerThingHandler) poller.getHandler()).getCommunicationInterface().getEndpoint(),
148                 task.getEndpoint());
149         assertSame(((ModbusPollerThingHandler) poller.getHandler()).getRequest(), task.getRequest());
150
151         addThing(poller);
152         return poller;
153     }
154
155     private Bridge createTcpMock() {
156         Bridge tcpBridge = ModbusPollerThingHandlerTest.createTcpThingBuilder("tcp1").build();
157         ModbusTcpThingHandler tcpThingHandler = Mockito.mock(ModbusTcpThingHandler.class);
158         tcpBridge.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
159         tcpBridge.setHandler(tcpThingHandler);
160         doReturn(comms).when(tcpThingHandler).getCommunicationInterface();
161         try {
162             doReturn(0).when(tcpThingHandler).getSlaveId();
163         } catch (EndpointNotInitializedException e) {
164             // not raised -- we are mocking return value only, not actually calling the method
165             throw new IllegalStateException();
166         }
167         tcpThingHandler.initialize();
168         assertThat(tcpBridge.getStatus(), is(equalTo(ThingStatus.ONLINE)));
169         return tcpBridge;
170     }
171
172     private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
173             Function<ThingBuilder, ThingBuilder> builderConfigurator) {
174         return createDataHandler(id, bridge, builderConfigurator, null);
175     }
176
177     private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
178             Function<ThingBuilder, ThingBuilder> builderConfigurator, BundleContext context) {
179         return createDataHandler(id, bridge, builderConfigurator, context, true);
180     }
181
182     private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
183             Function<ThingBuilder, ThingBuilder> builderConfigurator, BundleContext context,
184             boolean autoCreateItemsAndLinkToChannels) {
185         ThingUID thingUID = new ThingUID(THING_TYPE_MODBUS_DATA, id);
186         ThingBuilder builder = ThingBuilder.create(THING_TYPE_MODBUS_DATA, thingUID).withLabel("label for " + id);
187         Map<String, ChannelUID> toBeLinked = new HashMap<>();
188         for (Entry<String, String> entry : CHANNEL_TO_ACCEPTED_TYPE.entrySet()) {
189             String channelId = entry.getKey();
190             // accepted item type
191             String channelAcceptedType = entry.getValue();
192             ChannelUID channelUID = new ChannelUID(thingUID, channelId);
193             builder = builder.withChannel(ChannelBuilder.create(channelUID, channelAcceptedType).build());
194
195             if (autoCreateItemsAndLinkToChannels) {
196                 // Create item of correct type and link it to channel
197                 String itemName = getItemName(channelUID);
198                 final GenericItem item;
199                 item = coreItemFactory.createItem(channelAcceptedType, itemName);
200                 assertThat(String.format("Could not determine correct item type for %s", channelId), item,
201                         is(notNullValue()));
202                 assertNotNull(item);
203                 Objects.requireNonNull(item);
204                 addItem(item);
205                 toBeLinked.put(itemName, channelUID);
206             }
207         }
208         if (builderConfigurator != null) {
209             builder = builderConfigurator.apply(builder);
210         }
211
212         Thing dataThing = builder.withBridge(bridge.getUID()).build();
213         addThing(dataThing);
214
215         // Link after the things and items have been created
216         for (Entry<String, ChannelUID> entry : toBeLinked.entrySet()) {
217             linkItem(entry.getKey(), entry.getValue());
218         }
219         return (ModbusDataThingHandler) dataThing.getHandler();
220     }
221
222     private String getItemName(ChannelUID channelUID) {
223         return channelUID.toString().replace(':', '_') + "_item";
224     }
225
226     private void assertSingleStateUpdate(ModbusDataThingHandler handler, String channel, Matcher<State> matcher) {
227         waitForAssert(() -> {
228             ChannelUID channelUID = new ChannelUID(handler.getThing().getUID(), channel);
229             String itemName = getItemName(channelUID);
230             Item item = itemRegistry.get(itemName);
231             assertThat(String.format("Item %s is not available from item registry", itemName), item,
232                     is(notNullValue()));
233             assertNotNull(item);
234             List<State> updates = getStateUpdates(itemName);
235             if (updates != null) {
236                 assertThat(
237                         String.format("Many updates found, expected one: %s", Arrays.deepToString(updates.toArray())),
238                         updates.size(), is(equalTo(1)));
239             }
240             State state = updates == null ? null : updates.get(0);
241             assertThat(String.format("%s %s, state %s of type %s", item.getClass().getSimpleName(), itemName, state,
242                     state == null ? null : state.getClass().getSimpleName()), state, is(matcher));
243         });
244     }
245
246     private void assertSingleStateUpdate(ModbusDataThingHandler handler, String channel, State state) {
247         assertSingleStateUpdate(handler, channel, is(equalTo(state)));
248     }
249
250     private void testOutOfBoundsGeneric(int pollStart, int pollLength, String start,
251             ModbusReadFunctionCode functionCode, ValueType valueType, ThingStatus expectedStatus) {
252         testOutOfBoundsGeneric(pollStart, pollLength, start, functionCode, valueType, expectedStatus, null);
253     }
254
255     private void testOutOfBoundsGeneric(int pollStart, int pollLength, String start,
256             ModbusReadFunctionCode functionCode, ValueType valueType, ThingStatus expectedStatus,
257             BundleContext context) {
258         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
259
260         // Minimally mocked request
261         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
262         doReturn(pollStart).when(request).getReference();
263         doReturn(pollLength).when(request).getDataLength();
264         doReturn(functionCode).when(request).getFunctionCode();
265
266         PollTask task = Mockito.mock(PollTask.class);
267         doReturn(endpoint).when(task).getEndpoint();
268         doReturn(request).when(task).getRequest();
269
270         Bridge pollerThing = createPollerMock("poller1", task);
271
272         Configuration dataConfig = new Configuration();
273         dataConfig.put("readStart", start);
274         dataConfig.put("readTransform", "default");
275         dataConfig.put("readValueType", valueType.getConfigValue());
276         ModbusDataThingHandler dataHandler = createDataHandler("data1", pollerThing,
277                 builder -> builder.withConfiguration(dataConfig), context);
278         assertThat(dataHandler.getThing().getStatusInfo().getDescription(), dataHandler.getThing().getStatus(),
279                 is(equalTo(expectedStatus)));
280     }
281
282     @Test
283     public void testInitCoilsOutOfIndex() {
284         testOutOfBoundsGeneric(4, 3, "8", ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT,
285                 ThingStatus.OFFLINE);
286     }
287
288     @Test
289     public void testInitCoilsOutOfIndex2() {
290         // Reading coils 4, 5, 6. Coil 7 is out of bounds
291         testOutOfBoundsGeneric(4, 3, "7", ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT,
292                 ThingStatus.OFFLINE);
293     }
294
295     @Test
296     public void testInitCoilsOK() {
297         // Reading coils 4, 5, 6. Coil 6 is OK
298         testOutOfBoundsGeneric(4, 3, "6", ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT,
299                 ThingStatus.ONLINE);
300     }
301
302     @Test
303     public void testInitRegistersWithBitOutOfIndex() {
304         testOutOfBoundsGeneric(4, 3, "8.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
305                 ModbusConstants.ValueType.BIT, ThingStatus.OFFLINE);
306     }
307
308     @Test
309     public void testInitRegistersWithBitOutOfIndex2() {
310         testOutOfBoundsGeneric(4, 3, "7.16", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
311                 ModbusConstants.ValueType.BIT, ThingStatus.OFFLINE);
312     }
313
314     @Test
315     public void testInitRegistersWithBitOK() {
316         testOutOfBoundsGeneric(4, 3, "6.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
317                 ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
318     }
319
320     @Test
321     public void testInitRegistersWithBitOK2() {
322         testOutOfBoundsGeneric(4, 3, "6.15", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
323                 ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
324     }
325
326     @Test
327     public void testInitRegistersWithInt8OutOfIndex() {
328         testOutOfBoundsGeneric(4, 3, "8.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
329                 ModbusConstants.ValueType.INT8, ThingStatus.OFFLINE);
330     }
331
332     @Test
333     public void testInitRegistersWithInt8OutOfIndex2() {
334         testOutOfBoundsGeneric(4, 3, "7.2", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
335                 ModbusConstants.ValueType.INT8, ThingStatus.OFFLINE);
336     }
337
338     @Test
339     public void testInitRegistersWithInt8OK() {
340         testOutOfBoundsGeneric(4, 3, "6.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
341                 ModbusConstants.ValueType.INT8, ThingStatus.ONLINE);
342     }
343
344     @Test
345     public void testInitRegistersWithInt8OK2() {
346         testOutOfBoundsGeneric(4, 3, "6.1", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
347                 ModbusConstants.ValueType.INT8, ThingStatus.ONLINE);
348     }
349
350     @Test
351     public void testInitRegistersWithInt16OK() {
352         // Poller reading registers 4, 5, 6. Register 6 is OK
353         testOutOfBoundsGeneric(4, 3, "6", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
354                 ModbusConstants.ValueType.INT16, ThingStatus.ONLINE);
355     }
356
357     @Test
358     public void testInitRegistersWithInt16OutOfBounds() {
359         // Poller reading registers 4, 5, 6. Register 7 is out-of-bounds
360         testOutOfBoundsGeneric(4, 3, "7", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
361                 ModbusConstants.ValueType.INT16, ThingStatus.OFFLINE);
362     }
363
364     @Test
365     public void testInitRegistersWithInt16OutOfBounds2() {
366         testOutOfBoundsGeneric(4, 3, "8", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
367                 ModbusConstants.ValueType.INT16, ThingStatus.OFFLINE);
368     }
369
370     @Test
371     public void testInitRegistersWithInt16NoDecimalFormatAllowed() {
372         testOutOfBoundsGeneric(4, 3, "7.0", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
373                 ModbusConstants.ValueType.INT16, ThingStatus.OFFLINE);
374     }
375
376     @Test
377     public void testInitRegistersWithInt32OK() {
378         testOutOfBoundsGeneric(4, 3, "5", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
379                 ModbusConstants.ValueType.INT32, ThingStatus.ONLINE);
380     }
381
382     @Test
383     public void testInitRegistersWithInt32OutOfBounds() {
384         testOutOfBoundsGeneric(4, 3, "6", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
385                 ModbusConstants.ValueType.INT32, ThingStatus.OFFLINE);
386     }
387
388     @Test
389     public void testInitRegistersWithInt32AtTheEdge() {
390         testOutOfBoundsGeneric(4, 3, "5", ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
391                 ModbusConstants.ValueType.INT32, ThingStatus.ONLINE);
392     }
393
394     private ModbusDataThingHandler testReadHandlingGeneric(ModbusReadFunctionCode functionCode, String start,
395             String transform, ValueType valueType, BitArray bits, ModbusRegisterArray registers, Exception error) {
396         return testReadHandlingGeneric(functionCode, start, transform, valueType, bits, registers, error, null);
397     }
398
399     private ModbusDataThingHandler testReadHandlingGeneric(ModbusReadFunctionCode functionCode, String start,
400             String transform, ValueType valueType, BitArray bits, ModbusRegisterArray registers, Exception error,
401             BundleContext context) {
402         return testReadHandlingGeneric(functionCode, start, transform, valueType, bits, registers, error, context,
403                 true);
404     }
405
406     @SuppressWarnings({ "null" })
407     private ModbusDataThingHandler testReadHandlingGeneric(ModbusReadFunctionCode functionCode, String start,
408             String transform, ValueType valueType, BitArray bits, ModbusRegisterArray registers, Exception error,
409             BundleContext context, boolean autoCreateItemsAndLinkToChannels) {
410         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
411
412         int pollLength = 3;
413
414         // Minimally mocked request
415         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
416         doReturn(pollLength).when(request).getDataLength();
417         doReturn(functionCode).when(request).getFunctionCode();
418
419         PollTask task = Mockito.mock(PollTask.class);
420         doReturn(endpoint).when(task).getEndpoint();
421         doReturn(request).when(task).getRequest();
422
423         Bridge poller = createPollerMock("poller1", task);
424
425         Configuration dataConfig = new Configuration();
426         dataConfig.put("readStart", start);
427         dataConfig.put("readTransform", transform);
428         dataConfig.put("readValueType", valueType.getConfigValue());
429
430         String thingId = "read1";
431         ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
432                 builder -> builder.withConfiguration(dataConfig), context, autoCreateItemsAndLinkToChannels);
433
434         assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
435
436         // call callbacks
437         if (bits != null) {
438             assertNull(registers);
439             assertNull(error);
440             AsyncModbusReadResult result = new AsyncModbusReadResult(request, bits);
441             dataHandler.onReadResult(result);
442         } else if (registers != null) {
443             assertNull(bits);
444             assertNull(error);
445             AsyncModbusReadResult result = new AsyncModbusReadResult(request, registers);
446             dataHandler.onReadResult(result);
447         } else {
448             assertNull(bits);
449             assertNull(registers);
450             assertNotNull(error);
451             AsyncModbusFailure<ModbusReadRequestBlueprint> result = new AsyncModbusFailure<ModbusReadRequestBlueprint>(
452                     request, error);
453             dataHandler.handleReadError(result);
454         }
455         return dataHandler;
456     }
457
458     @SuppressWarnings({ "null" })
459     private ModbusDataThingHandler testWriteHandlingGeneric(String start, String transform, ValueType valueType,
460             String writeType, ModbusWriteFunctionCode successFC, String channel, Command command, Exception error,
461             BundleContext context) {
462         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
463
464         // Minimally mocked request
465         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
466
467         PollTask task = Mockito.mock(PollTask.class);
468         doReturn(endpoint).when(task).getEndpoint();
469         doReturn(request).when(task).getRequest();
470
471         Bridge poller = createPollerMock("poller1", task);
472
473         Configuration dataConfig = new Configuration();
474         dataConfig.put("readStart", "");
475         dataConfig.put("writeStart", start);
476         dataConfig.put("writeTransform", transform);
477         dataConfig.put("writeValueType", valueType.getConfigValue());
478         dataConfig.put("writeType", writeType);
479
480         String thingId = "write";
481
482         ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
483                 builder -> builder.withConfiguration(dataConfig), context);
484
485         assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
486
487         dataHandler.handleCommand(new ChannelUID(dataHandler.getThing().getUID(), channel), command);
488
489         if (error != null) {
490             dataHandler.handleReadError(new AsyncModbusFailure<ModbusReadRequestBlueprint>(request, error));
491         } else {
492             ModbusResponse resp = new ModbusResponse() {
493
494                 @Override
495                 public int getFunctionCode() {
496                     return successFC.getFunctionCode();
497                 }
498             };
499             dataHandler
500                     .onWriteResponse(new AsyncModbusWriteResult(Mockito.mock(ModbusWriteRequestBlueprint.class), resp));
501         }
502         return dataHandler;
503     }
504
505     @Test
506     public void testOnError() {
507         ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
508                 "0.0", "default", ModbusConstants.ValueType.BIT, null, null, new Exception("fooerror"));
509
510         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(notNullValue(State.class)));
511     }
512
513     @Test
514     public void testOnRegistersInt16StaticTransformation() {
515         ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
516                 "0", "-3", ModbusConstants.ValueType.INT16, null,
517                 new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null);
518
519         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
520         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
521
522         // -3 converts to "true"
523         assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, is(nullValue(State.class)));
524         assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, is(nullValue(State.class)));
525         assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, is(nullValue(State.class)));
526         assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, new DecimalType(-3));
527         // roller shutter fails since -3 is invalid value (not between 0...100)
528         // assertThatStateContains(state, CHANNEL_ROLLERSHUTTER, new PercentType(1));
529         assertSingleStateUpdate(dataHandler, CHANNEL_STRING, new StringType("-3"));
530         // no datetime, conversion not possible without transformation
531     }
532
533     @Test
534     public void testOnRegistersRealTransformation() {
535         mockTransformation("MULTIPLY", new MultiplyTransformation());
536         ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
537                 "0", "MULTIPLY(10)", ModbusConstants.ValueType.INT16, null,
538                 new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null, bundleContext);
539
540         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
541         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
542
543         // transformation output (-30) is not valid for contact or switch
544         assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, is(nullValue(State.class)));
545         assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, is(nullValue(State.class)));
546         // -30 is not valid value for Dimmer (PercentType) (not between 0...100)
547         assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, is(nullValue(State.class)));
548         assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, new DecimalType(-30));
549         // roller shutter fails since -3 is invalid value (not between 0...100)
550         assertSingleStateUpdate(dataHandler, CHANNEL_ROLLERSHUTTER, is(nullValue(State.class)));
551         assertSingleStateUpdate(dataHandler, CHANNEL_STRING, new StringType("-30"));
552         // no datetime, conversion not possible without transformation
553     }
554
555     @Test
556     public void testOnRegistersNaNFloatInRegisters() throws InvalidSyntaxException {
557         ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
558                 "0", "default", ModbusConstants.ValueType.FLOAT32, null, new ModbusRegisterArray(
559                         // equivalent of floating point NaN
560                         new byte[] { (byte) 0x7f, (byte) 0xc0, (byte) 0x00, (byte) 0x00 }),
561                 null, bundleContext);
562
563         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
564         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
565
566         // UNDEF is treated as "boolean true" (OPEN/ON) since it is != 0.
567         assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, OpenClosedType.OPEN);
568         assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, OnOffType.ON);
569         assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, OnOffType.ON);
570         assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, UnDefType.UNDEF);
571         assertSingleStateUpdate(dataHandler, CHANNEL_ROLLERSHUTTER, UnDefType.UNDEF);
572         assertSingleStateUpdate(dataHandler, CHANNEL_STRING, UnDefType.UNDEF);
573     }
574
575     @Test
576     public void testOnRegistersRealTransformation2() throws InvalidSyntaxException {
577         mockTransformation("ONOFF", new TransformationService() {
578
579             @Override
580             public String transform(String function, String source) throws TransformationException {
581                 return Integer.parseInt(source) != 0 ? "ON" : "OFF";
582             }
583         });
584         ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
585                 "0", "ONOFF(10)", ModbusConstants.ValueType.INT16, null,
586                 new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null, bundleContext);
587
588         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
589         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
590
591         assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, is(nullValue(State.class)));
592         assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, is(equalTo(OnOffType.ON)));
593         assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, is(equalTo(OnOffType.ON)));
594         assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, is(nullValue(State.class)));
595         assertSingleStateUpdate(dataHandler, CHANNEL_ROLLERSHUTTER, is(nullValue(State.class)));
596         assertSingleStateUpdate(dataHandler, CHANNEL_STRING, is(equalTo(new StringType("ON"))));
597     }
598
599     @Test
600     public void testWriteRealTransformation() throws InvalidSyntaxException {
601         captureModbusWrites();
602         mockTransformation("MULTIPLY", new MultiplyTransformation());
603         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "MULTIPLY(10)",
604                 ModbusConstants.ValueType.BIT, "coil", ModbusWriteFunctionCode.WRITE_COIL, "number",
605                 new DecimalType("2"), null, bundleContext);
606
607         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
608         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
609         assertThat(writeRequests.size(), is(equalTo(1)));
610         ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
611         assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL)));
612         assertThat(writeRequest.getReference(), is(equalTo(50)));
613         assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1)));
614         // Since transform output is non-zero, it is mapped as "true"
615         assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(true)));
616     }
617
618     @Test
619     public void testWriteRealTransformation2() throws InvalidSyntaxException {
620         captureModbusWrites();
621         mockTransformation("ZERO", new TransformationService() {
622
623             @Override
624             public String transform(String function, String source) throws TransformationException {
625                 return "0";
626             }
627         });
628         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "ZERO(foobar)",
629                 ModbusConstants.ValueType.BIT, "coil", ModbusWriteFunctionCode.WRITE_COIL, "number",
630                 new DecimalType("2"), null, bundleContext);
631
632         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
633         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
634         assertThat(writeRequests.size(), is(equalTo(1)));
635         ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
636         assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL)));
637         assertThat(writeRequest.getReference(), is(equalTo(50)));
638         assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1)));
639         // Since transform output is zero, it is mapped as "false"
640         assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(false)));
641     }
642
643     @Test
644     public void testWriteRealTransformation3() throws InvalidSyntaxException {
645         captureModbusWrites();
646         mockTransformation("RANDOM", new TransformationService() {
647
648             @Override
649             public String transform(String function, String source) throws TransformationException {
650                 return "5";
651             }
652         });
653         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "RANDOM(foobar)",
654                 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER, "number",
655                 new DecimalType("2"), null, bundleContext);
656
657         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
658         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
659         assertThat(writeRequests.size(), is(equalTo(1)));
660         ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
661         assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER)));
662         assertThat(writeRequest.getReference(), is(equalTo(50)));
663         assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1)));
664         assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0), is(equalTo(5)));
665     }
666
667     @Test
668     public void testWriteRealTransformation4() throws InvalidSyntaxException {
669         captureModbusWrites();
670         mockTransformation("JSON", new TransformationService() {
671
672             @Override
673             public String transform(String function, String source) throws TransformationException {
674                 return "[{"//
675                         + "\"functionCode\": 16,"//
676                         + "\"address\": 5412,"//
677                         + "\"value\": [1, 0, 5]"//
678                         + "},"//
679                         + "{"//
680                         + "\"functionCode\": 6,"//
681                         + "\"address\": 555,"//
682                         + "\"value\": [3]"//
683                         + "}]";
684             }
685         });
686         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "JSON(foobar)",
687                 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS, "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(2)));
693         {
694             ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
695             assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS)));
696             assertThat(writeRequest.getReference(), is(equalTo(5412)));
697             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(3)));
698             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0),
699                     is(equalTo(1)));
700             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(1),
701                     is(equalTo(0)));
702             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(2),
703                     is(equalTo(5)));
704         }
705         {
706             ModbusWriteRequestBlueprint writeRequest = writeRequests.get(1);
707             assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER)));
708             assertThat(writeRequest.getReference(), is(equalTo(555)));
709             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1)));
710             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0),
711                     is(equalTo(3)));
712         }
713     }
714
715     @Test
716     public void testWriteRealTransformation5() throws InvalidSyntaxException {
717         captureModbusWrites();
718         mockTransformation("PLUS", new TransformationService() {
719
720             @Override
721             public String transform(String arg, String source) throws TransformationException {
722                 return String.valueOf(Integer.parseInt(arg) + Integer.parseInt(source));
723             }
724         });
725         mockTransformation("CONCAT", new TransformationService() {
726
727             @Override
728             public String transform(String function, String source) throws TransformationException {
729                 return source + function;
730             }
731         });
732         mockTransformation("MULTIPLY", new MultiplyTransformation());
733         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "MULTIPLY:3∩PLUS(2)∩CONCAT(0)",
734                 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER, "number",
735                 new DecimalType("2"), null, bundleContext);
736
737         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
738         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
739         assertThat(writeRequests.size(), is(equalTo(1)));
740         ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
741         assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER)));
742         assertThat(writeRequest.getReference(), is(equalTo(50)));
743         assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1)));
744         assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0),
745                 is(equalTo(/* (2*3 + 2) + '0' */ 80)));
746     }
747
748     private void testValueTypeGeneric(ModbusReadFunctionCode functionCode, ValueType valueType,
749             ThingStatus expectedStatus) {
750         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
751
752         // Minimally mocked request
753         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
754         doReturn(3).when(request).getDataLength();
755         doReturn(functionCode).when(request).getFunctionCode();
756
757         PollTask task = Mockito.mock(PollTask.class);
758         doReturn(endpoint).when(task).getEndpoint();
759         doReturn(request).when(task).getRequest();
760
761         Bridge poller = createPollerMock("poller1", task);
762
763         Configuration dataConfig = new Configuration();
764         dataConfig.put("readStart", "1");
765         dataConfig.put("readTransform", "default");
766         dataConfig.put("readValueType", valueType.getConfigValue());
767         ModbusDataThingHandler dataHandler = createDataHandler("data1", poller,
768                 builder -> builder.withConfiguration(dataConfig));
769         assertThat(dataHandler.getThing().getStatus(), is(equalTo(expectedStatus)));
770     }
771
772     @Test
773     public void testCoilDoesNotAcceptFloat32ValueType() {
774         testValueTypeGeneric(ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.FLOAT32, ThingStatus.OFFLINE);
775     }
776
777     @Test
778     public void testCoilAcceptsBitValueType() {
779         testValueTypeGeneric(ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
780     }
781
782     @Test
783     public void testDiscreteInputDoesNotAcceptFloat32ValueType() {
784         testValueTypeGeneric(ModbusReadFunctionCode.READ_INPUT_DISCRETES, ModbusConstants.ValueType.FLOAT32,
785                 ThingStatus.OFFLINE);
786     }
787
788     @Test
789     public void testDiscreteInputAcceptsBitValueType() {
790         testValueTypeGeneric(ModbusReadFunctionCode.READ_INPUT_DISCRETES, ModbusConstants.ValueType.BIT,
791                 ThingStatus.ONLINE);
792     }
793
794     @Test
795     public void testRefreshOnData() throws InterruptedException {
796         ModbusReadFunctionCode functionCode = ModbusReadFunctionCode.READ_COILS;
797
798         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
799
800         int pollLength = 3;
801
802         // Minimally mocked request
803         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
804         doReturn(pollLength).when(request).getDataLength();
805         doReturn(functionCode).when(request).getFunctionCode();
806
807         PollTask task = Mockito.mock(PollTask.class);
808         doReturn(endpoint).when(task).getEndpoint();
809         doReturn(request).when(task).getRequest();
810
811         Bridge poller = createPollerMock("poller1", task);
812
813         Configuration dataConfig = new Configuration();
814         dataConfig.put("readStart", "0");
815         dataConfig.put("readTransform", "default");
816         dataConfig.put("readValueType", "bit");
817
818         String thingId = "read1";
819
820         ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
821                 builder -> builder.withConfiguration(dataConfig), bundleContext);
822         assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
823
824         verify(comms, never()).submitOneTimePoll(eq(request), notNull(), notNull());
825         // Wait for all channels to receive the REFRESH command (initiated by the core)
826         waitForAssert(
827                 () -> verify((ModbusPollerThingHandler) poller.getHandler(), times(CHANNEL_TO_ACCEPTED_TYPE.size()))
828                         .refresh());
829         // Reset the mock
830         reset(poller.getHandler());
831
832         // Issue REFRESH command and verify the results
833         dataHandler.handleCommand(Mockito.mock(ChannelUID.class), RefreshType.REFRESH);
834
835         // data handler asynchronously calls the poller.refresh() -- it might take some time
836         // We check that refresh is finally called
837         waitForAssert(() -> verify((ModbusPollerThingHandler) poller.getHandler()).refresh());
838     }
839
840     /**
841      *
842      * @param pollerFunctionCode poller function code. Use null if you want to have data thing direct child of endpoint
843      *            thing
844      * @param config thing config
845      * @param statusConsumer assertion method for data thingstatus
846      */
847     private void testInitGeneric(ModbusReadFunctionCode pollerFunctionCode, Configuration config,
848             Consumer<ThingStatusInfo> statusConsumer) {
849         int pollLength = 3;
850
851         Bridge parent;
852         if (pollerFunctionCode == null) {
853             parent = createTcpMock();
854             addThing(parent);
855         } else {
856             ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
857
858             // Minimally mocked request
859             ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
860             doReturn(pollLength).when(request).getDataLength();
861             doReturn(pollerFunctionCode).when(request).getFunctionCode();
862
863             PollTask task = Mockito.mock(PollTask.class);
864             doReturn(endpoint).when(task).getEndpoint();
865             doReturn(request).when(task).getRequest();
866
867             parent = createPollerMock("poller1", task);
868         }
869
870         String thingId = "read1";
871
872         ModbusDataThingHandler dataHandler = createDataHandler(thingId, parent,
873                 builder -> builder.withConfiguration(config), bundleContext);
874
875         statusConsumer.accept(dataHandler.getThing().getStatusInfo());
876     }
877
878     @Test
879     public void testReadOnlyData() {
880         Configuration dataConfig = new Configuration();
881         dataConfig.put("readStart", "0");
882         dataConfig.put("readValueType", "bit");
883         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
884                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
885     }
886
887     /**
888      * readValueType=bit should be assumed with coils, so it's ok to skip it
889      */
890     @Test
891     public void testReadOnlyDataMissingValueTypeWithCoils() {
892         Configuration dataConfig = new Configuration();
893         dataConfig.put("readStart", "0");
894         // missing value type
895         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
896                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
897     }
898
899     @Test
900     public void testReadOnlyDataInvalidValueType() {
901         Configuration dataConfig = new Configuration();
902         dataConfig.put("readStart", "0");
903         dataConfig.put("readValueType", "foobar");
904         testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
905             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
906             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
907         });
908     }
909
910     /**
911      * We do not assume value type with registers, not ok to skip it
912      */
913     @Test
914     public void testReadOnlyDataMissingValueTypeWithRegisters() {
915         Configuration dataConfig = new Configuration();
916         dataConfig.put("readStart", "0");
917         testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
918             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
919             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
920         });
921     }
922
923     @Test
924     public void testWriteOnlyData() {
925         Configuration dataConfig = new Configuration();
926         dataConfig.put("writeStart", "0");
927         dataConfig.put("writeValueType", "bit");
928         dataConfig.put("writeType", "coil");
929         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
930                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
931     }
932
933     @Test
934     public void testWriteHoldingInt16Data() {
935         Configuration dataConfig = new Configuration();
936         dataConfig.put("writeStart", "0");
937         dataConfig.put("writeValueType", "int16");
938         dataConfig.put("writeType", "holding");
939         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
940                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
941     }
942
943     @Test
944     public void testWriteHoldingInt8Data() {
945         Configuration dataConfig = new Configuration();
946         dataConfig.put("writeStart", "0");
947         dataConfig.put("writeValueType", "int8");
948         dataConfig.put("writeType", "holding");
949         testInitGeneric(null, dataConfig, status -> {
950             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
951             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
952         });
953     }
954
955     @Test
956     public void testWriteHoldingBitData() {
957         Configuration dataConfig = new Configuration();
958         dataConfig.put("writeStart", "0");
959         dataConfig.put("writeValueType", "bit");
960         dataConfig.put("writeType", "holding");
961         testInitGeneric(null, dataConfig, status -> {
962             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
963             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
964         });
965     }
966
967     @Test
968     public void testWriteOnlyDataChildOfEndpoint() {
969         Configuration dataConfig = new Configuration();
970         dataConfig.put("writeStart", "0");
971         dataConfig.put("writeValueType", "bit");
972         dataConfig.put("writeType", "coil");
973         testInitGeneric(null, dataConfig, status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
974     }
975
976     @Test
977     public void testWriteOnlyDataMissingOneParameter() {
978         Configuration dataConfig = new Configuration();
979         dataConfig.put("writeStart", "0");
980         dataConfig.put("writeValueType", "bit");
981         // missing writeType --> error
982         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
983             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
984             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
985             assertThat(status.getDescription(), is(not(equalTo(null))));
986         });
987     }
988
989     /**
990      * OK to omit writeValueType with coils since bit is assumed
991      */
992     @Test
993     public void testWriteOnlyDataMissingValueTypeWithCoilParameter() {
994         Configuration dataConfig = new Configuration();
995         dataConfig.put("writeStart", "0");
996         dataConfig.put("writeType", "coil");
997         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
998                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
999     }
1000
1001     @Test
1002     public void testWriteOnlyIllegalValueType() {
1003         Configuration dataConfig = new Configuration();
1004         dataConfig.put("writeStart", "0");
1005         dataConfig.put("writeType", "coil");
1006         dataConfig.put("writeValueType", "foobar");
1007         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1008             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1009             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1010         });
1011     }
1012
1013     @Test
1014     public void testWriteInvalidType() {
1015         Configuration dataConfig = new Configuration();
1016         dataConfig.put("writeStart", "0");
1017         dataConfig.put("writeType", "foobar");
1018         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1019             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1020             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1021         });
1022     }
1023
1024     @Test
1025     public void testWriteCoilBadStart() {
1026         Configuration dataConfig = new Configuration();
1027         dataConfig.put("writeStart", "0.4");
1028         dataConfig.put("writeType", "coil");
1029         testInitGeneric(null, dataConfig, status -> {
1030             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1031             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1032         });
1033     }
1034
1035     @Test
1036     public void testWriteHoldingBadStart() {
1037         Configuration dataConfig = new Configuration();
1038         dataConfig.put("writeStart", "0.4");
1039         dataConfig.put("writeType", "holding");
1040         testInitGeneric(null, dataConfig, status -> {
1041             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1042             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1043         });
1044     }
1045
1046     @Test
1047     public void testReadHoldingBadStart() {
1048         Configuration dataConfig = new Configuration();
1049         dataConfig.put("readStart", "0.0");
1050         dataConfig.put("readValueType", "int16");
1051         testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1052             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1053             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1054         });
1055     }
1056
1057     @Test
1058     public void testReadHoldingBadStart2() {
1059         Configuration dataConfig = new Configuration();
1060         dataConfig.put("readStart", "0.0");
1061         dataConfig.put("readValueType", "bit");
1062         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1063             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1064             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1065         });
1066     }
1067
1068     @Test
1069     public void testReadHoldingOKStart() {
1070         Configuration dataConfig = new Configuration();
1071         dataConfig.put("readStart", "0.0");
1072         dataConfig.put("readType", "holding");
1073         dataConfig.put("readValueType", "bit");
1074         testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig,
1075                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1076     }
1077
1078     @Test
1079     public void testReadValueTypeIllegal() {
1080         Configuration dataConfig = new Configuration();
1081         dataConfig.put("readStart", "0.0");
1082         dataConfig.put("readType", "holding");
1083         dataConfig.put("readValueType", "foobar");
1084         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1085             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1086             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1087         });
1088     }
1089
1090     @Test
1091     public void testWriteOnlyTransform() {
1092         Configuration dataConfig = new Configuration();
1093         // no need to have start, JSON output of transformation defines everything
1094         dataConfig.put("writeTransform", "JS(myJsonTransform.js)");
1095         testInitGeneric(null, dataConfig, status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1096     }
1097
1098     @Test
1099     public void testWriteTransformAndStart() {
1100         Configuration dataConfig = new Configuration();
1101         // It's illegal to have start and transform. Just have transform or have all
1102         dataConfig.put("writeStart", "3");
1103         dataConfig.put("writeTransform", "JS(myJsonTransform.js)");
1104         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1105             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1106             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1107         });
1108     }
1109
1110     @Test
1111     public void testWriteTransformAndNecessary() {
1112         Configuration dataConfig = new Configuration();
1113         // It's illegal to have start and transform. Just have transform or have all
1114         dataConfig.put("writeStart", "3");
1115         dataConfig.put("writeType", "holding");
1116         dataConfig.put("writeValueType", "int16");
1117         dataConfig.put("writeTransform", "JS(myJsonTransform.js)");
1118         testInitGeneric(null, dataConfig, status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1119     }
1120 }