]> git.basschouten.com Git - openhab-addons.git/blob
e87ceb3aed134ecec3e034ddc3fa11b2cd02311f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.modbus.tests;
14
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.*;
18 import static org.mockito.ArgumentMatchers.*;
19 import static org.mockito.ArgumentMatchers.any;
20 import static org.mockito.Mockito.*;
21 import static org.openhab.binding.modbus.internal.ModbusBindingConstantsInternal.*;
22
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.Objects;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.function.Consumer;
32 import java.util.function.Function;
33
34 import org.apache.commons.lang.StringUtils;
35 import org.hamcrest.Matcher;
36 import org.junit.jupiter.api.AfterEach;
37 import org.junit.jupiter.api.Disabled;
38 import org.junit.jupiter.api.Test;
39 import org.mockito.Mockito;
40 import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
41 import org.openhab.binding.modbus.handler.ModbusPollerThingHandler;
42 import org.openhab.binding.modbus.internal.handler.ModbusDataThingHandler;
43 import org.openhab.binding.modbus.internal.handler.ModbusTcpThingHandler;
44 import org.openhab.core.config.core.Configuration;
45 import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
46 import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
47 import org.openhab.core.io.transport.modbus.AsyncModbusWriteResult;
48 import org.openhab.core.io.transport.modbus.BitArray;
49 import org.openhab.core.io.transport.modbus.ModbusConstants;
50 import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
51 import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
52 import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
53 import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
54 import org.openhab.core.io.transport.modbus.ModbusResponse;
55 import org.openhab.core.io.transport.modbus.ModbusWriteCoilRequestBlueprint;
56 import org.openhab.core.io.transport.modbus.ModbusWriteFunctionCode;
57 import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
58 import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
59 import org.openhab.core.io.transport.modbus.PollTask;
60 import org.openhab.core.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
61 import org.openhab.core.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
62 import org.openhab.core.items.GenericItem;
63 import org.openhab.core.items.Item;
64 import org.openhab.core.library.items.DateTimeItem;
65 import org.openhab.core.library.types.DecimalType;
66 import org.openhab.core.library.types.OnOffType;
67 import org.openhab.core.library.types.OpenClosedType;
68 import org.openhab.core.library.types.StringType;
69 import org.openhab.core.thing.Bridge;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatus;
73 import org.openhab.core.thing.ThingStatusDetail;
74 import org.openhab.core.thing.ThingStatusInfo;
75 import org.openhab.core.thing.ThingUID;
76 import org.openhab.core.thing.binding.ThingHandler;
77 import org.openhab.core.thing.binding.builder.BridgeBuilder;
78 import org.openhab.core.thing.binding.builder.ChannelBuilder;
79 import org.openhab.core.thing.binding.builder.ThingBuilder;
80 import org.openhab.core.transform.TransformationException;
81 import org.openhab.core.transform.TransformationService;
82 import org.openhab.core.types.Command;
83 import org.openhab.core.types.RefreshType;
84 import org.openhab.core.types.State;
85 import org.openhab.core.types.UnDefType;
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, false);
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                 Objects.requireNonNull(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, false);
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, false);
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, false);
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 byte[] { (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 byte[] { (byte) 0xff, (byte) 0xfd }), null, 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     public void testOnRegistersNaNFloatInRegisters() throws InvalidSyntaxException {
565         ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
566                 "0", "default", ModbusConstants.ValueType.FLOAT32, null, new ModbusRegisterArray(
567                         // equivalent of floating point NaN
568                         new byte[] { (byte) 0x7f, (byte) 0xc0, (byte) 0x00, (byte) 0x00 }),
569                 null, bundleContext);
570
571         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
572         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
573
574         // UNDEF is treated as "boolean true" (OPEN/ON) since it is != 0.
575         assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, OpenClosedType.OPEN);
576         assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, OnOffType.ON);
577         assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, OnOffType.ON);
578         assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, UnDefType.UNDEF);
579         assertSingleStateUpdate(dataHandler, CHANNEL_ROLLERSHUTTER, UnDefType.UNDEF);
580         assertSingleStateUpdate(dataHandler, CHANNEL_STRING, UnDefType.UNDEF);
581     }
582
583     @Test
584     public void testOnRegistersRealTransformation2() throws InvalidSyntaxException {
585         mockTransformation("ONOFF", new TransformationService() {
586
587             @Override
588             public String transform(String function, String source) throws TransformationException {
589                 return Integer.parseInt(source) != 0 ? "ON" : "OFF";
590             }
591         });
592         ModbusDataThingHandler dataHandler = testReadHandlingGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
593                 "0", "ONOFF(10)", ModbusConstants.ValueType.INT16, null,
594                 new ModbusRegisterArray(new byte[] { (byte) 0xff, (byte) 0xfd }), null, bundleContext);
595
596         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_SUCCESS, is(notNullValue(State.class)));
597         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_READ_ERROR, is(nullValue(State.class)));
598
599         assertSingleStateUpdate(dataHandler, CHANNEL_CONTACT, is(nullValue(State.class)));
600         assertSingleStateUpdate(dataHandler, CHANNEL_SWITCH, is(equalTo(OnOffType.ON)));
601         assertSingleStateUpdate(dataHandler, CHANNEL_DIMMER, is(equalTo(OnOffType.ON)));
602         assertSingleStateUpdate(dataHandler, CHANNEL_NUMBER, is(nullValue(State.class)));
603         assertSingleStateUpdate(dataHandler, CHANNEL_ROLLERSHUTTER, is(nullValue(State.class)));
604         assertSingleStateUpdate(dataHandler, CHANNEL_STRING, is(equalTo(new StringType("ON"))));
605     }
606
607     @Test
608     public void testWriteRealTransformation() throws InvalidSyntaxException {
609         captureModbusWrites();
610         mockTransformation("MULTIPLY", new MultiplyTransformation());
611         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "MULTIPLY(10)",
612                 ModbusConstants.ValueType.BIT, "coil", ModbusWriteFunctionCode.WRITE_COIL, "number",
613                 new DecimalType("2"), null, bundleContext);
614
615         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
616         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
617         assertThat(writeRequests.size(), is(equalTo(1)));
618         ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
619         assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL)));
620         assertThat(writeRequest.getReference(), is(equalTo(50)));
621         assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1)));
622         // Since transform output is non-zero, it is mapped as "true"
623         assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(true)));
624     }
625
626     @Test
627     public void testWriteRealTransformation2() throws InvalidSyntaxException {
628         captureModbusWrites();
629         mockTransformation("ZERO", new TransformationService() {
630
631             @Override
632             public String transform(String function, String source) throws TransformationException {
633                 return "0";
634             }
635         });
636         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "ZERO(foobar)",
637                 ModbusConstants.ValueType.BIT, "coil", ModbusWriteFunctionCode.WRITE_COIL, "number",
638                 new DecimalType("2"), null, bundleContext);
639
640         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
641         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
642         assertThat(writeRequests.size(), is(equalTo(1)));
643         ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
644         assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_COIL)));
645         assertThat(writeRequest.getReference(), is(equalTo(50)));
646         assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().size(), is(equalTo(1)));
647         // Since transform output is zero, it is mapped as "false"
648         assertThat(((ModbusWriteCoilRequestBlueprint) writeRequest).getCoils().getBit(0), is(equalTo(false)));
649     }
650
651     @Test
652     public void testWriteRealTransformation3() throws InvalidSyntaxException {
653         captureModbusWrites();
654         mockTransformation("RANDOM", new TransformationService() {
655
656             @Override
657             public String transform(String function, String source) throws TransformationException {
658                 return "5";
659             }
660         });
661         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "RANDOM(foobar)",
662                 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER, "number",
663                 new DecimalType("2"), null, bundleContext);
664
665         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
666         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
667         assertThat(writeRequests.size(), is(equalTo(1)));
668         ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
669         assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER)));
670         assertThat(writeRequest.getReference(), is(equalTo(50)));
671         assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1)));
672         assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0), is(equalTo(5)));
673     }
674
675     @Test
676     public void testWriteRealTransformation4() throws InvalidSyntaxException {
677         captureModbusWrites();
678         mockTransformation("JSON", new TransformationService() {
679
680             @Override
681             public String transform(String function, String source) throws TransformationException {
682                 return "[{"//
683                         + "\"functionCode\": 16,"//
684                         + "\"address\": 5412,"//
685                         + "\"value\": [1, 0, 5]"//
686                         + "},"//
687                         + "{"//
688                         + "\"functionCode\": 6,"//
689                         + "\"address\": 555,"//
690                         + "\"value\": [3]"//
691                         + "}]";
692             }
693         });
694         ModbusDataThingHandler dataHandler = testWriteHandlingGeneric("50", "JSON(foobar)",
695                 ModbusConstants.ValueType.INT16, "holding", ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS, "number",
696                 new DecimalType("2"), null, bundleContext);
697
698         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_SUCCESS, is(notNullValue(State.class)));
699         assertSingleStateUpdate(dataHandler, CHANNEL_LAST_WRITE_ERROR, is(nullValue(State.class)));
700         assertThat(writeRequests.size(), is(equalTo(2)));
701         {
702             ModbusWriteRequestBlueprint writeRequest = writeRequests.get(0);
703             assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_MULTIPLE_REGISTERS)));
704             assertThat(writeRequest.getReference(), is(equalTo(5412)));
705             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(3)));
706             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0),
707                     is(equalTo(1)));
708             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(1),
709                     is(equalTo(0)));
710             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(2),
711                     is(equalTo(5)));
712         }
713         {
714             ModbusWriteRequestBlueprint writeRequest = writeRequests.get(1);
715             assertThat(writeRequest.getFunctionCode(), is(equalTo(ModbusWriteFunctionCode.WRITE_SINGLE_REGISTER)));
716             assertThat(writeRequest.getReference(), is(equalTo(555)));
717             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().size(), is(equalTo(1)));
718             assertThat(((ModbusWriteRegisterRequestBlueprint) writeRequest).getRegisters().getRegister(0),
719                     is(equalTo(3)));
720         }
721     }
722
723     private void testValueTypeGeneric(ModbusReadFunctionCode functionCode, ValueType valueType,
724             ThingStatus expectedStatus) {
725         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
726
727         // Minimally mocked request
728         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
729         doReturn(3).when(request).getDataLength();
730         doReturn(functionCode).when(request).getFunctionCode();
731
732         PollTask task = Mockito.mock(PollTask.class);
733         doReturn(endpoint).when(task).getEndpoint();
734         doReturn(request).when(task).getRequest();
735
736         Bridge poller = createPollerMock("poller1", task);
737
738         Configuration dataConfig = new Configuration();
739         dataConfig.put("readStart", "1");
740         dataConfig.put("readTransform", "default");
741         dataConfig.put("readValueType", valueType.getConfigValue());
742         ModbusDataThingHandler dataHandler = createDataHandler("data1", poller,
743                 builder -> builder.withConfiguration(dataConfig));
744         assertThat(dataHandler.getThing().getStatus(), is(equalTo(expectedStatus)));
745     }
746
747     @Test
748     public void testCoilDoesNotAcceptFloat32ValueType() {
749         testValueTypeGeneric(ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.FLOAT32, ThingStatus.OFFLINE);
750     }
751
752     @Test
753     public void testCoilAcceptsBitValueType() {
754         testValueTypeGeneric(ModbusReadFunctionCode.READ_COILS, ModbusConstants.ValueType.BIT, ThingStatus.ONLINE);
755     }
756
757     @Test
758     public void testDiscreteInputDoesNotAcceptFloat32ValueType() {
759         testValueTypeGeneric(ModbusReadFunctionCode.READ_INPUT_DISCRETES, ModbusConstants.ValueType.FLOAT32,
760                 ThingStatus.OFFLINE);
761     }
762
763     @Test
764     public void testDiscreteInputAcceptsBitValueType() {
765         testValueTypeGeneric(ModbusReadFunctionCode.READ_INPUT_DISCRETES, ModbusConstants.ValueType.BIT,
766                 ThingStatus.ONLINE);
767     }
768
769     @Test
770     @Disabled("See: https://github.com/openhab/openhab-addons/issues/9617")
771     public void testRefreshOnData() throws InterruptedException {
772         ModbusReadFunctionCode functionCode = ModbusReadFunctionCode.READ_COILS;
773
774         ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
775
776         int pollLength = 3;
777
778         // Minimally mocked request
779         ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
780         doReturn(pollLength).when(request).getDataLength();
781         doReturn(functionCode).when(request).getFunctionCode();
782
783         PollTask task = Mockito.mock(PollTask.class);
784         doReturn(endpoint).when(task).getEndpoint();
785         doReturn(request).when(task).getRequest();
786
787         Bridge poller = createPollerMock("poller1", task);
788
789         Configuration dataConfig = new Configuration();
790         dataConfig.put("readStart", "0");
791         dataConfig.put("readTransform", "default");
792         dataConfig.put("readValueType", "bit");
793
794         String thingId = "read1";
795
796         ModbusDataThingHandler dataHandler = createDataHandler(thingId, poller,
797                 builder -> builder.withConfiguration(dataConfig), bundleContext);
798         assertThat(dataHandler.getThing().getStatus(), is(equalTo(ThingStatus.ONLINE)));
799
800         verify(comms, never()).submitOneTimePoll(eq(request), notNull(), notNull());
801         // Reset initial REFRESH commands to data thing channels from the Core
802         reset(poller.getHandler());
803         dataHandler.handleCommand(Mockito.mock(ChannelUID.class), RefreshType.REFRESH);
804
805         // data handler asynchronously calls the poller.refresh() -- it might take some time
806         // We check that refresh is finally called
807         waitForAssert(() -> verify((ModbusPollerThingHandler) poller.getHandler()).refresh(), 2500, 50);
808     }
809
810     /**
811      *
812      * @param pollerFunctionCode poller function code. Use null if you want to have data thing direct child of endpoint
813      *            thing
814      * @param config thing config
815      * @param statusConsumer assertion method for data thingstatus
816      */
817     private void testInitGeneric(ModbusReadFunctionCode pollerFunctionCode, Configuration config,
818             Consumer<ThingStatusInfo> statusConsumer) {
819         int pollLength = 3;
820
821         Bridge parent;
822         if (pollerFunctionCode == null) {
823             parent = createTcpMock();
824             ThingHandler foo = parent.getHandler();
825             addThing(parent);
826         } else {
827             ModbusSlaveEndpoint endpoint = new ModbusTCPSlaveEndpoint("thisishost", 502, false);
828
829             // Minimally mocked request
830             ModbusReadRequestBlueprint request = Mockito.mock(ModbusReadRequestBlueprint.class);
831             doReturn(pollLength).when(request).getDataLength();
832             doReturn(pollerFunctionCode).when(request).getFunctionCode();
833
834             PollTask task = Mockito.mock(PollTask.class);
835             doReturn(endpoint).when(task).getEndpoint();
836             doReturn(request).when(task).getRequest();
837
838             parent = createPollerMock("poller1", task);
839         }
840
841         String thingId = "read1";
842
843         ModbusDataThingHandler dataHandler = createDataHandler(thingId, parent,
844                 builder -> builder.withConfiguration(config), bundleContext);
845
846         statusConsumer.accept(dataHandler.getThing().getStatusInfo());
847     }
848
849     @Test
850     public void testReadOnlyData() {
851         Configuration dataConfig = new Configuration();
852         dataConfig.put("readStart", "0");
853         dataConfig.put("readValueType", "bit");
854         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
855                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
856     }
857
858     /**
859      * readValueType=bit should be assumed with coils, so it's ok to skip it
860      */
861     @Test
862     public void testReadOnlyDataMissingValueTypeWithCoils() {
863         Configuration dataConfig = new Configuration();
864         dataConfig.put("readStart", "0");
865         // missing value type
866         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
867                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
868     }
869
870     @Test
871     public void testReadOnlyDataInvalidValueType() {
872         Configuration dataConfig = new Configuration();
873         dataConfig.put("readStart", "0");
874         dataConfig.put("readValueType", "foobar");
875         testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
876             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
877             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
878         });
879     }
880
881     /**
882      * We do not assume value type with registers, not ok to skip it
883      */
884     @Test
885     public void testReadOnlyDataMissingValueTypeWithRegisters() {
886         Configuration dataConfig = new Configuration();
887         dataConfig.put("readStart", "0");
888         testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
889             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
890             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
891         });
892     }
893
894     @Test
895     public void testWriteOnlyData() {
896         Configuration dataConfig = new Configuration();
897         dataConfig.put("writeStart", "0");
898         dataConfig.put("writeValueType", "bit");
899         dataConfig.put("writeType", "coil");
900         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
901                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
902     }
903
904     @Test
905     public void testWriteHoldingInt16Data() {
906         Configuration dataConfig = new Configuration();
907         dataConfig.put("writeStart", "0");
908         dataConfig.put("writeValueType", "int16");
909         dataConfig.put("writeType", "holding");
910         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
911                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
912     }
913
914     @Test
915     public void testWriteHoldingInt8Data() {
916         Configuration dataConfig = new Configuration();
917         dataConfig.put("writeStart", "0");
918         dataConfig.put("writeValueType", "int8");
919         dataConfig.put("writeType", "holding");
920         testInitGeneric(null, dataConfig, status -> {
921             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
922             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
923         });
924     }
925
926     @Test
927     public void testWriteHoldingBitData() {
928         Configuration dataConfig = new Configuration();
929         dataConfig.put("writeStart", "0");
930         dataConfig.put("writeValueType", "bit");
931         dataConfig.put("writeType", "holding");
932         testInitGeneric(null, dataConfig, status -> {
933             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
934             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
935         });
936     }
937
938     @Test
939     public void testWriteOnlyDataChildOfEndpoint() {
940         Configuration dataConfig = new Configuration();
941         dataConfig.put("writeStart", "0");
942         dataConfig.put("writeValueType", "bit");
943         dataConfig.put("writeType", "coil");
944         testInitGeneric(null, dataConfig, status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
945     }
946
947     @Test
948     public void testWriteOnlyDataMissingOneParameter() {
949         Configuration dataConfig = new Configuration();
950         dataConfig.put("writeStart", "0");
951         dataConfig.put("writeValueType", "bit");
952         // missing writeType --> error
953         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
954             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
955             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
956             assertThat(status.getDescription(), is(not(equalTo(null))));
957         });
958     }
959
960     /**
961      * OK to omit writeValueType with coils since bit is assumed
962      */
963     @Test
964     public void testWriteOnlyDataMissingValueTypeWithCoilParameter() {
965         Configuration dataConfig = new Configuration();
966         dataConfig.put("writeStart", "0");
967         dataConfig.put("writeType", "coil");
968         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig,
969                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
970     }
971
972     @Test
973     public void testWriteOnlyIllegalValueType() {
974         Configuration dataConfig = new Configuration();
975         dataConfig.put("writeStart", "0");
976         dataConfig.put("writeType", "coil");
977         dataConfig.put("writeValueType", "foobar");
978         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
979             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
980             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
981         });
982     }
983
984     @Test
985     public void testWriteInvalidType() {
986         Configuration dataConfig = new Configuration();
987         dataConfig.put("writeStart", "0");
988         dataConfig.put("writeType", "foobar");
989         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
990             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
991             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
992         });
993     }
994
995     @Test
996     public void testWriteCoilBadStart() {
997         Configuration dataConfig = new Configuration();
998         dataConfig.put("writeStart", "0.4");
999         dataConfig.put("writeType", "coil");
1000         testInitGeneric(null, dataConfig, status -> {
1001             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1002             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1003         });
1004     }
1005
1006     @Test
1007     public void testWriteHoldingBadStart() {
1008         Configuration dataConfig = new Configuration();
1009         dataConfig.put("writeStart", "0.4");
1010         dataConfig.put("writeType", "holding");
1011         testInitGeneric(null, dataConfig, status -> {
1012             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1013             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1014         });
1015     }
1016
1017     @Test
1018     public void testReadHoldingBadStart() {
1019         Configuration dataConfig = new Configuration();
1020         dataConfig.put("readStart", "0.0");
1021         dataConfig.put("readValueType", "int16");
1022         testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig, status -> {
1023             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1024             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1025         });
1026     }
1027
1028     @Test
1029     public void testReadHoldingBadStart2() {
1030         Configuration dataConfig = new Configuration();
1031         dataConfig.put("readStart", "0.0");
1032         dataConfig.put("readValueType", "bit");
1033         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1034             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1035             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1036         });
1037     }
1038
1039     @Test
1040     public void testReadHoldingOKStart() {
1041         Configuration dataConfig = new Configuration();
1042         dataConfig.put("readStart", "0.0");
1043         dataConfig.put("readType", "holding");
1044         dataConfig.put("readValueType", "bit");
1045         testInitGeneric(ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, dataConfig,
1046                 status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1047     }
1048
1049     @Test
1050     public void testReadValueTypeIllegal() {
1051         Configuration dataConfig = new Configuration();
1052         dataConfig.put("readStart", "0.0");
1053         dataConfig.put("readType", "holding");
1054         dataConfig.put("readValueType", "foobar");
1055         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1056             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1057             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1058         });
1059     }
1060
1061     @Test
1062     public void testWriteOnlyTransform() {
1063         Configuration dataConfig = new Configuration();
1064         // no need to have start, JSON output of transformation defines everything
1065         dataConfig.put("writeTransform", "JS(myJsonTransform.js)");
1066         testInitGeneric(null, dataConfig, status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1067     }
1068
1069     @Test
1070     public void testWriteTransformAndStart() {
1071         Configuration dataConfig = new Configuration();
1072         // It's illegal to have start and transform. Just have transform or have all
1073         dataConfig.put("writeStart", "3");
1074         dataConfig.put("writeTransform", "JS(myJsonTransform.js)");
1075         testInitGeneric(ModbusReadFunctionCode.READ_COILS, dataConfig, status -> {
1076             assertThat(status.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
1077             assertThat(status.getStatusDetail(), is(equalTo(ThingStatusDetail.CONFIGURATION_ERROR)));
1078         });
1079     }
1080
1081     @Test
1082     public void testWriteTransformAndNecessary() {
1083         Configuration dataConfig = new Configuration();
1084         // It's illegal to have start and transform. Just have transform or have all
1085         dataConfig.put("writeStart", "3");
1086         dataConfig.put("writeType", "holding");
1087         dataConfig.put("writeValueType", "int16");
1088         dataConfig.put("writeTransform", "JS(myJsonTransform.js)");
1089         testInitGeneric(null, dataConfig, status -> assertThat(status.getStatus(), is(equalTo(ThingStatus.ONLINE))));
1090     }
1091 }