]> git.basschouten.com Git - openhab-addons.git/blob
c402d0c00dc539b146409964362bef70fbad07a4
[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.internal.profiles;
14
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.junit.jupiter.api.Assumptions.*;
17 import static org.mockito.Mockito.*;
18
19 import java.util.Optional;
20 import java.util.stream.Stream;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.junit.jupiter.api.Test;
25 import org.junit.jupiter.params.ParameterizedTest;
26 import org.junit.jupiter.params.provider.Arguments;
27 import org.junit.jupiter.params.provider.EmptySource;
28 import org.junit.jupiter.params.provider.MethodSource;
29 import org.junit.jupiter.params.provider.NullSource;
30 import org.mockito.ArgumentCaptor;
31 import org.openhab.core.config.core.Configuration;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.thing.profiles.ProfileCallback;
36 import org.openhab.core.thing.profiles.ProfileContext;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.types.RefreshType;
39 import org.openhab.core.types.State;
40 import org.openhab.core.types.Type;
41 import org.openhab.core.types.UnDefType;
42
43 /**
44  * @author Sami Salonen - Initial contribution
45  */
46 @NonNullByDefault
47 public class ModbusGainOffsetProfileTest {
48
49     private static Stream<Arguments> provideArgsForBoth() {
50         return Stream.of(
51                 // dimensionless
52                 Arguments.of("100", "0.5", "250", "175.0"), Arguments.of("0", "1 %", "250", "250 %"),
53                 //
54                 // gain with same unit
55                 //
56                 // e.g. (handler) 3 <---> (item) 106K with pre-gain-offset=50, gain=2K
57                 // e.g. (handler) 3 K <---> (item) 106K^2 with pre-gain-offset=50K, gain=2K
58                 //
59                 Arguments.of("50", "2 K", "3", "106 K"),
60                 //
61                 // gain with different unit
62                 //
63                 Arguments.of("50", "2 m/s", "3", "106 m/s"),
64                 //
65                 // gain without unit
66                 //
67                 Arguments.of("50", "2", "3", "106"),
68                 //
69                 // temperature tests
70                 //
71                 // celsius gain
72                 Arguments.of("0", "0.1 °C", "25", "2.5 °C"),
73                 // kelvin gain
74                 Arguments.of("0", "0.1 K", "25", "2.5 K"),
75                 // fahrenheit gain
76                 Arguments.of("0", "10 °F", "0.18", "1.80 °F"),
77                 //
78                 // unsupported types are passed with error
79                 Arguments.of("0", "0", OnOffType.ON, OnOffType.ON)
80
81         );
82     }
83
84     private static Stream<Arguments> provideAdditionalArgsForStateUpdateFromHandler() {
85         return Stream.of(
86
87                 // Dimensionless conversion 2.5/1% = 250%/1% = 250
88                 Arguments.of("0", "1 %", "250", "250 %"), Arguments.of("2 %", "1 %", "249.9800", "250.0000 %"),
89                 Arguments.of("50", "2 m/s", new DecimalType("3"), "106 m/s"),
90                 // UNDEF passes the profile unchanged
91                 Arguments.of("0", "0", UnDefType.UNDEF, UnDefType.UNDEF));
92     }
93
94     /**
95      *
96      * Test profile behaviour when handler updates the state
97      *
98      */
99     @ParameterizedTest
100     @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
101     public void testOnStateUpdateFromHandler(String preGainOffset, String gain, Object updateFromHandlerObj,
102             Object expectedUpdateTowardsItemObj) {
103         testOnUpdateFromHandlerGeneric(preGainOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, true);
104     }
105
106     /**
107      *
108      * Test profile behaviour when handler sends command
109      *
110      */
111     @ParameterizedTest
112     @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
113     public void testOnCommandFromHandler(String preGainOffset, String gain, Object updateFromHandlerObj,
114             Object expectedUpdateTowardsItemObj) {
115         // UNDEF is not a command, cannot be sent by handler
116         assumeTrue(updateFromHandlerObj != UnDefType.UNDEF);
117         testOnUpdateFromHandlerGeneric(preGainOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, false);
118     }
119
120     /**
121      *
122      * Test profile behaviour when handler updates the state
123      *
124      * @param preGainOffset profile pre-gain-offset offset
125      * @param gain profile gain
126      * @param updateFromHandlerObj state update from handler. String representing QuantityType or State/Command
127      * @param expectedUpdateTowardsItemObj expected state/command update towards item. String representing QuantityType
128      *            or
129      *            State
130      * @param stateUpdateFromHandler whether there is state update from handler. Otherwise command
131      */
132     @SuppressWarnings("rawtypes")
133     private void testOnUpdateFromHandlerGeneric(String preGainOffset, String gain, Object updateFromHandlerObj,
134             Object expectedUpdateTowardsItemObj, boolean stateUpdateFromHandler) {
135         ProfileCallback callback = mock(ProfileCallback.class);
136         ModbusGainOffsetProfile profile = createProfile(callback, gain, preGainOffset);
137
138         final Type actualStateUpdateTowardsItem;
139         if (stateUpdateFromHandler) {
140             final State updateFromHandler;
141             if (updateFromHandlerObj instanceof String) {
142                 updateFromHandler = new QuantityType((String) updateFromHandlerObj);
143             } else {
144                 assertTrue(updateFromHandlerObj instanceof State);
145                 updateFromHandler = (State) updateFromHandlerObj;
146             }
147
148             profile.onStateUpdateFromHandler(updateFromHandler);
149
150             ArgumentCaptor<State> capture = ArgumentCaptor.forClass(State.class);
151             verify(callback, times(1)).sendUpdate(capture.capture());
152             actualStateUpdateTowardsItem = capture.getValue();
153         } else {
154             final Command updateFromHandler;
155             if (updateFromHandlerObj instanceof String) {
156                 updateFromHandler = new QuantityType((String) updateFromHandlerObj);
157             } else {
158                 assertTrue(updateFromHandlerObj instanceof State);
159                 updateFromHandler = (Command) updateFromHandlerObj;
160             }
161
162             profile.onCommandFromHandler(updateFromHandler);
163
164             ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
165             verify(callback, times(1)).sendCommand(capture.capture());
166             actualStateUpdateTowardsItem = capture.getValue();
167         }
168
169         Type expectedStateUpdateTowardsItem = (expectedUpdateTowardsItemObj instanceof String)
170                 ? new QuantityType((String) expectedUpdateTowardsItemObj)
171                 : (Type) expectedUpdateTowardsItemObj;
172         // Workaround for errors like "java.lang.UnsupportedOperationException: °C is non-linear, cannot convert"
173         if (expectedStateUpdateTowardsItem instanceof QuantityType<?>) {
174             assertTrue(actualStateUpdateTowardsItem instanceof QuantityType<?>);
175             assertEquals(((QuantityType<?>) expectedStateUpdateTowardsItem).getUnit(),
176                     ((QuantityType<?>) actualStateUpdateTowardsItem).getUnit());
177             assertEquals(((QuantityType<?>) expectedStateUpdateTowardsItem).toBigDecimal(),
178                     ((QuantityType<?>) actualStateUpdateTowardsItem).toBigDecimal());
179         } else {
180             assertEquals(expectedStateUpdateTowardsItem, actualStateUpdateTowardsItem);
181         }
182         verifyNoMoreInteractions(callback);
183     }
184
185     private static Stream<Arguments> provideAdditionalArgsForCommandFromItem() {
186         return Stream.of(
187                 // Dimensionless conversion 2.5/1% = 250%/1% = 250
188                 // gain in %, command as bare ratio and the other way around
189                 Arguments.of("0", "1 %", "250", "2.5"), Arguments.of("2%", "1 %", "249.9800", "2.5"),
190
191                 // celsius gain, kelvin command
192                 Arguments.of("0", "0.1 °C", "-2706.5", "2.5 K"),
193
194                 // incompatible command unit, should be convertible with gain
195                 Arguments.of("0", "0.1 °C", null, "2.5 m/s"),
196                 //
197                 // incompatible offset unit
198                 //
199                 Arguments.of("50 K", "21", null, "30 m/s"), Arguments.of("50 m/s", "21", null, "30 K"),
200                 //
201                 // UNDEF command is not processed
202                 //
203                 Arguments.of("0", "0", null, UnDefType.UNDEF),
204                 //
205                 // REFRESH command is forwarded
206                 //
207                 Arguments.of("0", "0", RefreshType.REFRESH, RefreshType.REFRESH)
208
209         );
210     }
211
212     /**
213      *
214      * Test profile behaviour when item receives command
215      *
216      * @param preGainOffset profile pre-gain-offset
217      * @param gain profile gain
218      * @param expectedCommandTowardsHandlerObj expected command towards handler. String representing QuantityType or
219      *            Command. Use null to verify that no commands are sent to handler.
220      * @param commandFromItemObj command that item receives. String representing QuantityType or Command.
221      */
222     @SuppressWarnings({ "rawtypes" })
223     @ParameterizedTest
224     @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForCommandFromItem" })
225     public void testOnCommandFromItem(String preGainOffset, String gain,
226             @Nullable Object expectedCommandTowardsHandlerObj, Object commandFromItemObj) {
227         assumeFalse(commandFromItemObj.equals(UnDefType.UNDEF));
228         ProfileCallback callback = mock(ProfileCallback.class);
229         ModbusGainOffsetProfile profile = createProfile(callback, gain, preGainOffset);
230
231         Command commandFromItem = (commandFromItemObj instanceof String) ? new QuantityType((String) commandFromItemObj)
232                 : (Command) commandFromItemObj;
233         profile.onCommandFromItem(commandFromItem);
234
235         boolean callsExpected = expectedCommandTowardsHandlerObj != null;
236         if (callsExpected) {
237             ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
238             verify(callback, times(1)).handleCommand(capture.capture());
239             Command actualCommandTowardsHandler = capture.getValue();
240             Command expectedCommandTowardsHandler = (expectedCommandTowardsHandlerObj instanceof String)
241                     ? new QuantityType((String) expectedCommandTowardsHandlerObj)
242                     : (Command) expectedCommandTowardsHandlerObj;
243             assertEquals(expectedCommandTowardsHandler, actualCommandTowardsHandler);
244             verifyNoMoreInteractions(callback);
245         } else {
246             verifyNoInteractions(callback);
247         }
248     }
249
250     /**
251      *
252      * Test behaviour when item receives state update from item (no-op)
253      *
254      **/
255     @Test
256     public void testOnCommandFromItem() {
257         ProfileCallback callback = mock(ProfileCallback.class);
258         ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0");
259
260         profile.onStateUpdateFromItem(new DecimalType(3.78));
261         // should be no-op
262         verifyNoInteractions(callback);
263     }
264
265     @Test
266     public void testInvalidInit() {
267         // preGainOffset must be dimensionless
268         ProfileCallback callback = mock(ProfileCallback.class);
269         ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0 K");
270         assertFalse(profile.isValid());
271     }
272
273     @ParameterizedTest
274     @NullSource
275     @EmptySource
276     public void testInitGainDefault(String gain) {
277         ProfileCallback callback = mock(ProfileCallback.class);
278         ModbusGainOffsetProfile<?> p = createProfile(callback, gain, "0.0");
279         assertTrue(p.isValid());
280         assertEquals(p.getGain(), Optional.of(QuantityType.ONE));
281     }
282
283     @ParameterizedTest
284     @NullSource
285     @EmptySource
286     public void testInitOffsetDefault(String preGainOffset) {
287         ProfileCallback callback = mock(ProfileCallback.class);
288         ModbusGainOffsetProfile<?> p = createProfile(callback, "1", preGainOffset);
289         assertTrue(p.isValid());
290         assertEquals(p.getPregainOffset(), Optional.of(QuantityType.ZERO));
291     }
292
293     private ModbusGainOffsetProfile<?> createProfile(ProfileCallback callback, @Nullable String gain,
294             @Nullable String preGainOffset) {
295         ProfileContext context = mock(ProfileContext.class);
296         Configuration config = new Configuration();
297         if (gain != null) {
298             config.put("gain", gain);
299         }
300         if (preGainOffset != null) {
301             config.put("pre-gain-offset", preGainOffset);
302         }
303         when(context.getConfiguration()).thenReturn(config);
304
305         return new ModbusGainOffsetProfile<>(callback, context);
306     }
307 }