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