]> git.basschouten.com Git - openhab-addons.git/blob
5506dbb867da7d86686c41c6fcbcae6785332133
[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(expectedStateUpdateTowardsItem, actualStateUpdateTowardsItem);
176         } else {
177             assertEquals(expectedStateUpdateTowardsItem, actualStateUpdateTowardsItem);
178         }
179         verifyNoMoreInteractions(callback);
180     }
181
182     private static Stream<Arguments> provideAdditionalArgsForCommandFromItem() {
183         return Stream.of(
184                 // Dimensionless conversion 2.5/1% = 250%/1% = 250
185                 // gain in %, command as bare ratio and the other way around
186                 Arguments.of("0", "1 %", "250", "2.5"), Arguments.of("2%", "1 %", "249.9800", "2.5"),
187
188                 // celsius gain, kelvin command
189                 Arguments.of("0", "0.1 °C", "-2706.5", "2.5 K"),
190
191                 // incompatible command unit, should be convertible with gain
192                 Arguments.of("0", "0.1 °C", null, "2.5 m/s"),
193                 //
194                 // incompatible offset unit
195                 //
196                 Arguments.of("50 K", "21", null, "30 m/s"), Arguments.of("50 m/s", "21", null, "30 K"),
197                 //
198                 // UNDEF command is not processed
199                 //
200                 Arguments.of("0", "0", null, UnDefType.UNDEF),
201                 //
202                 // REFRESH command is forwarded
203                 //
204                 Arguments.of("0", "0", RefreshType.REFRESH, RefreshType.REFRESH)
205
206         );
207     }
208
209     /**
210      *
211      * Test profile behaviour when item receives command
212      *
213      * @param preGainOffset profile pre-gain-offset
214      * @param gain profile gain
215      * @param expectedCommandTowardsHandlerObj expected command towards handler. String representing QuantityType or
216      *            Command. Use null to verify that no commands are sent to handler.
217      * @param commandFromItemObj command that item receives. String representing QuantityType or Command.
218      */
219     @SuppressWarnings({ "rawtypes" })
220     @ParameterizedTest
221     @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForCommandFromItem" })
222     public void testOnCommandFromItem(String preGainOffset, String gain,
223             @Nullable Object expectedCommandTowardsHandlerObj, Object commandFromItemObj) {
224         assumeFalse(commandFromItemObj.equals(UnDefType.UNDEF));
225         ProfileCallback callback = mock(ProfileCallback.class);
226         ModbusGainOffsetProfile profile = createProfile(callback, gain, preGainOffset);
227
228         Command commandFromItem = (commandFromItemObj instanceof String) ? new QuantityType((String) commandFromItemObj)
229                 : (Command) commandFromItemObj;
230         profile.onCommandFromItem(commandFromItem);
231
232         boolean callsExpected = expectedCommandTowardsHandlerObj != null;
233         if (callsExpected) {
234             ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
235             verify(callback, times(1)).handleCommand(capture.capture());
236             Command actualCommandTowardsHandler = capture.getValue();
237             Command expectedCommandTowardsHandler = (expectedCommandTowardsHandlerObj instanceof String)
238                     ? new QuantityType((String) expectedCommandTowardsHandlerObj)
239                     : (Command) expectedCommandTowardsHandlerObj;
240             assertEquals(expectedCommandTowardsHandler, actualCommandTowardsHandler);
241             verifyNoMoreInteractions(callback);
242         } else {
243             verifyNoInteractions(callback);
244         }
245     }
246
247     /**
248      *
249      * Test behaviour when item receives state update from item (no-op)
250      *
251      **/
252     @Test
253     public void testOnCommandFromItem() {
254         ProfileCallback callback = mock(ProfileCallback.class);
255         ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0");
256
257         profile.onStateUpdateFromItem(new DecimalType(3.78));
258         // should be no-op
259         verifyNoInteractions(callback);
260     }
261
262     @Test
263     public void testInvalidInit() {
264         // preGainOffset must be dimensionless
265         ProfileCallback callback = mock(ProfileCallback.class);
266         ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0 K");
267         assertFalse(profile.isValid());
268     }
269
270     @ParameterizedTest
271     @NullSource
272     @EmptySource
273     public void testInitGainDefault(String gain) {
274         ProfileCallback callback = mock(ProfileCallback.class);
275         ModbusGainOffsetProfile<?> p = createProfile(callback, gain, "0.0");
276         assertTrue(p.isValid());
277         assertEquals(p.getGain(), Optional.of(QuantityType.ONE));
278     }
279
280     @ParameterizedTest
281     @NullSource
282     @EmptySource
283     public void testInitOffsetDefault(String preGainOffset) {
284         ProfileCallback callback = mock(ProfileCallback.class);
285         ModbusGainOffsetProfile<?> p = createProfile(callback, "1", preGainOffset);
286         assertTrue(p.isValid());
287         assertEquals(p.getPregainOffset(), Optional.of(QuantityType.ZERO));
288     }
289
290     private ModbusGainOffsetProfile<?> createProfile(ProfileCallback callback, @Nullable String gain,
291             @Nullable String preGainOffset) {
292         ProfileContext context = mock(ProfileContext.class);
293         Configuration config = new Configuration();
294         if (gain != null) {
295             config.put("gain", gain);
296         }
297         if (preGainOffset != null) {
298             config.put("pre-gain-offset", preGainOffset);
299         }
300         when(context.getConfiguration()).thenReturn(config);
301
302         return new ModbusGainOffsetProfile<>(callback, context);
303     }
304 }