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