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