]> git.basschouten.com Git - openhab-addons.git/blob
2d2edecf7ce8741cc630dfc4ea4ef37d81a71bcb
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.modbus.tests;
14
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.*;
18 import static org.mockito.ArgumentMatchers.*;
19 import static org.mockito.ArgumentMatchers.any;
20 import static org.mockito.Mockito.*;
21 import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*;
22
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.function.Consumer;
31 import java.util.function.Function;
32
33 import org.apache.commons.lang.StringUtils;
34 import org.hamcrest.Matcher;
35 import org.junit.jupiter.api.AfterEach;
36 import org.junit.jupiter.api.Disabled;
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.items.GenericItem;
45 import org.openhab.core.items.Item;
46 import org.openhab.core.library.items.DateTimeItem;
47 import org.openhab.core.library.types.DecimalType;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.library.types.OpenClosedType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.thing.Bridge;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.ThingStatusInfo;
57 import org.openhab.core.thing.ThingUID;
58 import org.openhab.core.thing.binding.ThingHandler;
59 import org.openhab.core.thing.binding.builder.BridgeBuilder;
60 import org.openhab.core.thing.binding.builder.ChannelBuilder;
61 import org.openhab.core.thing.binding.builder.ThingBuilder;
62 import org.openhab.core.transform.TransformationException;
63 import org.openhab.core.transform.TransformationService;
64 import org.openhab.core.types.Command;
65 import org.openhab.core.types.RefreshType;
66 import org.openhab.core.types.State;
67 import org.openhab.core.types.UnDefType;
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 public class ModbusDataHandlerTest extends AbstractModbusOSGiTest {
93
94     private final class MultiplyTransformation implements TransformationService {
95         @Override
96         public String transform(String function, String source) throws TransformationException {
97             return String.valueOf(Integer.parseInt(function) * Integer.parseInt(source));
98         }
99     }
100
101     private static final Map<String, String> CHANNEL_TO_ACCEPTED_TYPE = new HashMap<>();
102     static {
103         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_SWITCH, "Switch");
104         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_CONTACT, "Contact");
105         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_DATETIME, "DateTime");
106         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_DIMMER, "Dimmer");
107         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_NUMBER, "Number");
108         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_STRING, "String");
109         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_ROLLERSHUTTER, "Rollershutter");
110         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_READ_SUCCESS, "DateTime");
111         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_WRITE_SUCCESS, "DateTime");
112         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_WRITE_ERROR, "DateTime");
113         CHANNEL_TO_ACCEPTED_TYPE.put(CHANNEL_LAST_READ_ERROR, "DateTime");
114     }
115     private List<ModbusWriteRequestBlueprint> writeRequests = new ArrayList<>();
116
117     @AfterEach
118     public void tearDown() {
119         writeRequests.clear();
120     }
121
122     private void captureModbusWrites() {
123         Mockito.when(comms.submitOneTimeWrite(any(), any(), any())).then(invocation -> {
124             ModbusWriteRequestBlueprint task = (ModbusWriteRequestBlueprint) invocation.getArgument(0);
125             writeRequests.add(task);
126             return Mockito.mock(ScheduledFuture.class);
127         });
128     }
129
130     private Bridge createPollerMock(String pollerId, PollTask task) {
131         final Bridge poller;
132         ThingUID thingUID = new ThingUID(THING_TYPE_MODBUS_POLLER, pollerId);
133         BridgeBuilder builder = BridgeBuilder.create(THING_TYPE_MODBUS_POLLER, thingUID)
134                 .withLabel("label for " + pollerId);
135         for (Entry<String, String> entry : CHANNEL_TO_ACCEPTED_TYPE.entrySet()) {
136             String channelId = entry.getKey();
137             String channelAcceptedType = entry.getValue();
138             builder = builder.withChannel(
139                     ChannelBuilder.create(new ChannelUID(thingUID, channelId), channelAcceptedType).build());
140         }
141         poller = builder.build();
142         poller.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
143
144         ModbusPollerThingHandler mockHandler = Mockito.mock(ModbusPollerThingHandler.class);
145         doReturn(task.getRequest()).when(mockHandler).getRequest();
146         assert comms != null;
147         doReturn(comms).when(mockHandler).getCommunicationInterface();
148         doReturn(task.getEndpoint()).when(comms).getEndpoint();
149         poller.setHandler(mockHandler);
150         assertSame(poller.getHandler(), mockHandler);
151         assertSame(((ModbusPollerThingHandler) poller.getHandler()).getCommunicationInterface().getEndpoint(),
152                 task.getEndpoint());
153         assertSame(((ModbusPollerThingHandler) poller.getHandler()).getRequest(), task.getRequest());
154
155         addThing(poller);
156         return poller;
157     }
158
159     private Bridge createTcpMock() {
160         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502);
161         Bridge tcpBridge = ModbusPollerThingHandlerTest.createTcpThingBuilder("tcp1").build();
162         ModbusTcpThingHandler tcpThingHandler = Mockito.mock(ModbusTcpThingHandler.class);
163         tcpBridge.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""));
164         tcpBridge.setHandler(tcpThingHandler);
165         doReturn(comms).when(tcpThingHandler).getCommunicationInterface();
166         try {
167             doReturn(0).when(tcpThingHandler).getSlaveId();
168         } catch (EndpointNotInitializedException e) {
169             // not raised -- we are mocking return value only, not actually calling the method
170             throw new IllegalStateException();
171         }
172         tcpThingHandler.initialize();
173         assertThat(tcpBridge.getStatus(), is(equalTo(ThingStatus.ONLINE)));
174         return tcpBridge;
175     }
176
177     private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
178             Function<ThingBuilder, ThingBuilder> builderConfigurator) {
179         return createDataHandler(id, bridge, builderConfigurator, null);
180     }
181
182     private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
183             Function<ThingBuilder, ThingBuilder> builderConfigurator, BundleContext context) {
184         return createDataHandler(id, bridge, builderConfigurator, context, true);
185     }
186
187     private ModbusDataThingHandler createDataHandler(String id, Bridge bridge,
188             Function<ThingBuilder, ThingBuilder> builderConfigurator, BundleContext context,
189             boolean autoCreateItemsAndLinkToChannels) {
190         ThingUID thingUID = new ThingUID(THING_TYPE_MODBUS_DATA, id);
191         ThingBuilder builder = ThingBuilder.create(THING_TYPE_MODBUS_DATA, thingUID).withLabel("label for " + id);
192         Map<String, ChannelUID> toBeLinked = new HashMap<>();
193         for (Entry<String, String> entry : CHANNEL_TO_ACCEPTED_TYPE.entrySet()) {
194             String channelId = entry.getKey();
195             String channelAcceptedType = entry.getValue();
196             ChannelUID channelUID = new ChannelUID(thingUID, channelId);
197             builder = builder.withChannel(ChannelBuilder.create(channelUID, channelAcceptedType).build());
198
199             if (autoCreateItemsAndLinkToChannels) {
200                 // Create item of correct type and link it to channel
201                 String itemName = getItemName(channelUID);
202                 final GenericItem item;
203                 if (channelId.startsWith("last") || channelId.equals("datetime")) {
204                     item = new DateTimeItem(itemName);
205                 } else {
206                     item = coreItemFactory.createItem(StringUtils.capitalize(channelId), itemName);
207                 }
208                 assertThat(String.format("Could not determine correct item type for %s", channelId), item,
209                         is(notNullValue()));
210                 assertNotNull(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);
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);
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);
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 ModbusRegister[] { new ModbusRegister((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 ModbusRegister[] { new ModbusRegister((byte) 0xff, (byte) 0xfd) }), null,
546                 bundleContext);
547
548         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
549         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
550
551         // transformation output (-30) is not valid for contact or switch
552         assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, is(nullValue(State.class)));
553         assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, is(nullValue(State.class)));
554         // -30 is not valid value for Dimmer (PercentType) (not between 0...100)
555         assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, is(nullValue(State.class)));
556         assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, new DecimalType(-30));
557         // roller shutter fails since -3 is invalid value (not between 0...100)
558         assertSingleStateUpdate(dataHandler, CHANNEL_ROLLERSHUTTER, is(nullValue(State.class)));
559         assertSingleStateUpdate(dataHandler, CHANNEL_STRING, new StringType("-30"));
560         // no datetime, conversion not possible without transformation
561     }
562
563     @Test
564     @Disabled("See: https://github.com/openhab/openhab-addons/issues/8880")
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 }