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