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