]> git.basschouten.com Git - openhab-addons.git/blob
5f0faddb8b0a67b124d485cbd4e888f5f6ba19a3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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     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     static Stream<Arguments> provideAdditionalArgsForStateUpdateFromHandler() {
85         return Stream.of(
86                 // Dimensionless conversion 2.5/1% = 250%/1% = 250
87                 Arguments.of("0", "1 %", "250", "250 %"), Arguments.of("2 %", "1 %", "249.9800", "250.0000 %"),
88                 Arguments.of("50", "2 m/s", new DecimalType("3"), "106 m/s"),
89                 // UNDEF passes the profile unchanged
90                 Arguments.of("0", "0", UnDefType.UNDEF, UnDefType.UNDEF));
91     }
92
93     /**
94      *
95      * Test profile behaviour when handler updates the state
96      *
97      */
98     @ParameterizedTest
99     @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
100     public void testOnStateUpdateFromHandler(String preGainOffset, String gain, Object updateFromHandlerObj,
101             Object expectedUpdateTowardsItemObj) {
102         testOnUpdateFromHandlerGeneric(preGainOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, true);
103     }
104
105     /**
106      *
107      * Test profile behaviour when handler sends command
108      *
109      */
110     @ParameterizedTest
111     @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
112     public void testOnCommandFromHandler(String preGainOffset, String gain, Object updateFromHandlerObj,
113             Object expectedUpdateTowardsItemObj) {
114         // UNDEF is not a command, cannot be sent by handler
115         assumeTrue(updateFromHandlerObj != UnDefType.UNDEF);
116         testOnUpdateFromHandlerGeneric(preGainOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, false);
117     }
118
119     /**
120      *
121      * Test profile behaviour when handler updates the state
122      *
123      * @param preGainOffset profile pre-gain-offset offset
124      * @param gain profile gain
125      * @param updateFromHandlerObj state update from handler. String representing QuantityType or State/Command
126      * @param expectedUpdateTowardsItemObj expected state/command update towards item. String representing QuantityType
127      *            or
128      *            State
129      * @param stateUpdateFromHandler whether there is state update from handler. Otherwise command
130      */
131     @SuppressWarnings("rawtypes")
132     private void testOnUpdateFromHandlerGeneric(String preGainOffset, String gain, Object updateFromHandlerObj,
133             Object expectedUpdateTowardsItemObj, boolean stateUpdateFromHandler) {
134         ProfileCallback callback = mock(ProfileCallback.class);
135         ModbusGainOffsetProfile profile = createProfile(callback, gain, preGainOffset);
136
137         final Type actualStateUpdateTowardsItem;
138         if (stateUpdateFromHandler) {
139             final State updateFromHandler;
140             if (updateFromHandlerObj instanceof String str) {
141                 updateFromHandler = new QuantityType(str);
142             } else {
143                 assertTrue(updateFromHandlerObj instanceof State);
144                 updateFromHandler = (State) updateFromHandlerObj;
145             }
146
147             profile.onStateUpdateFromHandler(updateFromHandler);
148
149             ArgumentCaptor<State> capture = ArgumentCaptor.forClass(State.class);
150             verify(callback, times(1)).sendUpdate(capture.capture());
151             actualStateUpdateTowardsItem = capture.getValue();
152         } else {
153             final Command updateFromHandler;
154             if (updateFromHandlerObj instanceof String str) {
155                 updateFromHandler = new QuantityType(str);
156             } else {
157                 assertTrue(updateFromHandlerObj instanceof State);
158                 updateFromHandler = (Command) updateFromHandlerObj;
159             }
160
161             profile.onCommandFromHandler(updateFromHandler);
162
163             ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
164             verify(callback, times(1)).sendCommand(capture.capture());
165             actualStateUpdateTowardsItem = capture.getValue();
166         }
167
168         Type expectedStateUpdateTowardsItem = (expectedUpdateTowardsItemObj instanceof String s) ? new QuantityType(s)
169                 : (Type) expectedUpdateTowardsItemObj;
170         assertEquals(expectedStateUpdateTowardsItem, actualStateUpdateTowardsItem);
171         verifyNoMoreInteractions(callback);
172     }
173
174     static Stream<Arguments> provideAdditionalArgsForCommandFromItem() {
175         return Stream.of(
176                 // Dimensionless conversion 2.5/1% = 250%/1% = 250
177                 // gain in %, command as bare ratio and the other way around
178                 Arguments.of("0", "1 %", "250", "2.5"), Arguments.of("2%", "1 %", "249.9800", "2.5"),
179
180                 // celsius gain, kelvin command
181                 Arguments.of("0", "0.1 °C", "-2706.5", "2.5 K"),
182
183                 // incompatible command unit, should be convertible with gain
184                 Arguments.of("0", "0.1 °C", null, "2.5 m/s"),
185                 //
186                 // incompatible offset unit
187                 //
188                 Arguments.of("50 K", "21", null, "30 m/s"), Arguments.of("50 m/s", "21", null, "30 K"),
189                 //
190                 // UNDEF command is not processed
191                 //
192                 Arguments.of("0", "0", null, UnDefType.UNDEF),
193                 //
194                 // REFRESH command is forwarded
195                 //
196                 Arguments.of("0", "0", RefreshType.REFRESH, RefreshType.REFRESH)
197
198         );
199     }
200
201     /**
202      *
203      * Test profile behavior when item receives command
204      *
205      * @param preGainOffset profile pre-gain-offset
206      * @param gain profile gain
207      * @param expectedCommandTowardsHandlerObj expected command towards handler. String representing QuantityType or
208      *            Command. Use null to verify that no commands are sent to handler.
209      * @param commandFromItemObj command that item receives. String representing QuantityType or Command.
210      */
211     @SuppressWarnings({ "rawtypes" })
212     @ParameterizedTest
213     @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForCommandFromItem" })
214     public void testOnCommandFromItem(String preGainOffset, String gain,
215             @Nullable Object expectedCommandTowardsHandlerObj, Object commandFromItemObj) {
216         assumeFalse(commandFromItemObj.equals(UnDefType.UNDEF));
217         ProfileCallback callback = mock(ProfileCallback.class);
218         ModbusGainOffsetProfile profile = createProfile(callback, gain, preGainOffset);
219
220         Command commandFromItem = (commandFromItemObj instanceof String str) ? new QuantityType(str)
221                 : (Command) commandFromItemObj;
222         profile.onCommandFromItem(commandFromItem);
223
224         boolean callsExpected = expectedCommandTowardsHandlerObj != null;
225         if (callsExpected) {
226             ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
227             verify(callback, times(1)).handleCommand(capture.capture());
228             Command actualCommandTowardsHandler = capture.getValue();
229             Command expectedCommandTowardsHandler = (expectedCommandTowardsHandlerObj instanceof String str)
230                     ? new QuantityType(str)
231                     : (Command) expectedCommandTowardsHandlerObj;
232             assertEquals(expectedCommandTowardsHandler, actualCommandTowardsHandler);
233             verifyNoMoreInteractions(callback);
234         } else {
235             verifyNoInteractions(callback);
236         }
237     }
238
239     /**
240      *
241      * Test behaviour when item receives state update from item (no-op)
242      *
243      **/
244     @Test
245     public void testOnCommandFromItem() {
246         ProfileCallback callback = mock(ProfileCallback.class);
247         ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0");
248
249         profile.onStateUpdateFromItem(new DecimalType(3.78));
250         // should be no-op
251         verifyNoInteractions(callback);
252     }
253
254     @Test
255     public void testInvalidInit() {
256         // preGainOffset must be dimensionless
257         ProfileCallback callback = mock(ProfileCallback.class);
258         ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0 K");
259         assertFalse(profile.isValid());
260     }
261
262     @ParameterizedTest
263     @NullSource
264     @EmptySource
265     public void testInitGainDefault(String gain) {
266         ProfileCallback callback = mock(ProfileCallback.class);
267         ModbusGainOffsetProfile<?> p = createProfile(callback, gain, "0.0");
268         assertTrue(p.isValid());
269         assertEquals(p.getGain(), Optional.of(QuantityType.ONE));
270     }
271
272     @ParameterizedTest
273     @NullSource
274     @EmptySource
275     public void testInitOffsetDefault(String preGainOffset) {
276         ProfileCallback callback = mock(ProfileCallback.class);
277         ModbusGainOffsetProfile<?> p = createProfile(callback, "1", preGainOffset);
278         assertTrue(p.isValid());
279         assertEquals(p.getPregainOffset(), Optional.of(QuantityType.ZERO));
280     }
281
282     private ModbusGainOffsetProfile<?> createProfile(ProfileCallback callback, @Nullable String gain,
283             @Nullable String preGainOffset) {
284         ProfileContext context = mock(ProfileContext.class);
285         Configuration config = new Configuration();
286         if (gain != null) {
287             config.put("gain", gain);
288         }
289         if (preGainOffset != null) {
290             config.put("pre-gain-offset", preGainOffset);
291         }
292         when(context.getConfiguration()).thenReturn(config);
293
294         return new ModbusGainOffsetProfile<>(callback, context);
295     }
296 }