2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.modbus.internal.profiles;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.junit.jupiter.api.Assumptions.*;
17 import static org.mockito.Mockito.*;
19 import java.util.Optional;
20 import java.util.stream.Stream;
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;
44 * @author Sami Salonen - Initial contribution
47 public class ModbusGainOffsetProfileTest {
49 private static Stream<Arguments> provideArgsForBoth() {
52 Arguments.of("100", "0.5", "250", "175.0"), Arguments.of("0", "1 %", "250", "250 %"),
54 // gain with same unit
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
59 Arguments.of("50", "2 K", "3", "106 K"),
61 // gain with different unit
63 Arguments.of("50", "2 m/s", "3", "106 m/s"),
67 Arguments.of("50", "2", "3", "106"),
72 Arguments.of("0", "0.1 °C", "25", "2.5 °C"),
74 Arguments.of("0", "0.1 K", "25", "2.5 K"),
76 Arguments.of("0", "10 °F", "0.18", "1.80 °F"),
78 // unsupported types are passed with error
79 Arguments.of("0", "0", OnOffType.ON, OnOffType.ON)
84 private static Stream<Arguments> provideAdditionalArgsForStateUpdateFromHandler() {
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));
96 * Test profile behaviour when handler updates the state
100 @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
101 public void testOnStateUpdateFromHandler(String preGainOffset, String gain, Object updateFromHandlerObj,
102 Object expectedUpdateTowardsItemObj) {
103 testOnUpdateFromHandlerGeneric(preGainOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, true);
108 * Test profile behaviour when handler sends command
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);
122 * Test profile behaviour when handler updates the state
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
130 * @param stateUpdateFromHandler whether there is state update from handler. Otherwise command
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);
138 final Type actualStateUpdateTowardsItem;
139 if (stateUpdateFromHandler) {
140 final State updateFromHandler;
141 if (updateFromHandlerObj instanceof String) {
142 updateFromHandler = new QuantityType((String) updateFromHandlerObj);
144 assertTrue(updateFromHandlerObj instanceof State);
145 updateFromHandler = (State) updateFromHandlerObj;
148 profile.onStateUpdateFromHandler(updateFromHandler);
150 ArgumentCaptor<State> capture = ArgumentCaptor.forClass(State.class);
151 verify(callback, times(1)).sendUpdate(capture.capture());
152 actualStateUpdateTowardsItem = capture.getValue();
154 final Command updateFromHandler;
155 if (updateFromHandlerObj instanceof String) {
156 updateFromHandler = new QuantityType((String) updateFromHandlerObj);
158 assertTrue(updateFromHandlerObj instanceof State);
159 updateFromHandler = (Command) updateFromHandlerObj;
162 profile.onCommandFromHandler(updateFromHandler);
164 ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
165 verify(callback, times(1)).sendCommand(capture.capture());
166 actualStateUpdateTowardsItem = capture.getValue();
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());
180 assertEquals(expectedStateUpdateTowardsItem, actualStateUpdateTowardsItem);
182 verifyNoMoreInteractions(callback);
185 private static Stream<Arguments> provideAdditionalArgsForCommandFromItem() {
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"),
191 // celsius gain, kelvin command
192 Arguments.of("0", "0.1 °C", "-2706.5", "2.5 K"),
194 // incompatible command unit, should be convertible with gain
195 Arguments.of("0", "0.1 °C", null, "2.5 m/s"),
197 // incompatible offset unit
199 Arguments.of("50 K", "21", null, "30 m/s"), Arguments.of("50 m/s", "21", null, "30 K"),
201 // UNDEF command is not processed
203 Arguments.of("0", "0", null, UnDefType.UNDEF),
205 // REFRESH command is forwarded
207 Arguments.of("0", "0", RefreshType.REFRESH, RefreshType.REFRESH)
214 * Test profile behaviour when item receives command
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.
222 @SuppressWarnings({ "rawtypes" })
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);
231 Command commandFromItem = (commandFromItemObj instanceof String) ? new QuantityType((String) commandFromItemObj)
232 : (Command) commandFromItemObj;
233 profile.onCommandFromItem(commandFromItem);
235 boolean callsExpected = expectedCommandTowardsHandlerObj != null;
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);
246 verifyNoInteractions(callback);
252 * Test behaviour when item receives state update from item (no-op)
256 public void testOnCommandFromItem() {
257 ProfileCallback callback = mock(ProfileCallback.class);
258 ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0");
260 profile.onStateUpdateFromItem(new DecimalType(3.78));
262 verifyNoInteractions(callback);
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());
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));
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));
293 private ModbusGainOffsetProfile<?> createProfile(ProfileCallback callback, @Nullable String gain,
294 @Nullable String preGainOffset) {
295 ProfileContext context = mock(ProfileContext.class);
296 Configuration config = new Configuration();
298 config.put("gain", gain);
300 if (preGainOffset != null) {
301 config.put("pre-gain-offset", preGainOffset);
303 when(context.getConfiguration()).thenReturn(config);
305 return new ModbusGainOffsetProfile<>(callback, context);