2 * Copyright (c) 2010-2023 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 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 static Stream<Arguments> provideAdditionalArgsForStateUpdateFromHandler() {
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));
95 * Test profile behaviour when handler updates the state
99 @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForStateUpdateFromHandler" })
100 public void testOnStateUpdateFromHandler(String preGainOffset, String gain, Object updateFromHandlerObj,
101 Object expectedUpdateTowardsItemObj) {
102 testOnUpdateFromHandlerGeneric(preGainOffset, gain, updateFromHandlerObj, expectedUpdateTowardsItemObj, true);
107 * Test profile behaviour when handler sends command
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);
121 * Test profile behaviour when handler updates the state
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
129 * @param stateUpdateFromHandler whether there is state update from handler. Otherwise command
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);
137 final Type actualStateUpdateTowardsItem;
138 if (stateUpdateFromHandler) {
139 final State updateFromHandler;
140 if (updateFromHandlerObj instanceof String str) {
141 updateFromHandler = new QuantityType(str);
143 assertTrue(updateFromHandlerObj instanceof State);
144 updateFromHandler = (State) updateFromHandlerObj;
147 profile.onStateUpdateFromHandler(updateFromHandler);
149 ArgumentCaptor<State> capture = ArgumentCaptor.forClass(State.class);
150 verify(callback, times(1)).sendUpdate(capture.capture());
151 actualStateUpdateTowardsItem = capture.getValue();
153 final Command updateFromHandler;
154 if (updateFromHandlerObj instanceof String str) {
155 updateFromHandler = new QuantityType(str);
157 assertTrue(updateFromHandlerObj instanceof State);
158 updateFromHandler = (Command) updateFromHandlerObj;
161 profile.onCommandFromHandler(updateFromHandler);
163 ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
164 verify(callback, times(1)).sendCommand(capture.capture());
165 actualStateUpdateTowardsItem = capture.getValue();
168 Type expectedStateUpdateTowardsItem = (expectedUpdateTowardsItemObj instanceof String s) ? new QuantityType(s)
169 : (Type) expectedUpdateTowardsItemObj;
170 assertEquals(expectedStateUpdateTowardsItem, actualStateUpdateTowardsItem);
171 verifyNoMoreInteractions(callback);
174 static Stream<Arguments> provideAdditionalArgsForCommandFromItem() {
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"),
180 // celsius gain, kelvin command
181 Arguments.of("0", "0.1 °C", "-2706.5", "2.5 K"),
183 // incompatible command unit, should be convertible with gain
184 Arguments.of("0", "0.1 °C", null, "2.5 m/s"),
186 // incompatible offset unit
188 Arguments.of("50 K", "21", null, "30 m/s"), Arguments.of("50 m/s", "21", null, "30 K"),
190 // UNDEF command is not processed
192 Arguments.of("0", "0", null, UnDefType.UNDEF),
194 // REFRESH command is forwarded
196 Arguments.of("0", "0", RefreshType.REFRESH, RefreshType.REFRESH)
203 * Test profile behavior when item receives command
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.
211 @SuppressWarnings({ "rawtypes" })
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);
220 Command commandFromItem = (commandFromItemObj instanceof String str) ? new QuantityType(str)
221 : (Command) commandFromItemObj;
222 profile.onCommandFromItem(commandFromItem);
224 boolean callsExpected = expectedCommandTowardsHandlerObj != null;
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);
235 verifyNoInteractions(callback);
241 * Test behaviour when item receives state update from item (no-op)
245 public void testOnCommandFromItem() {
246 ProfileCallback callback = mock(ProfileCallback.class);
247 ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0");
249 profile.onStateUpdateFromItem(new DecimalType(3.78));
251 verifyNoInteractions(callback);
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());
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));
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));
282 private ModbusGainOffsetProfile<?> createProfile(ProfileCallback callback, @Nullable String gain,
283 @Nullable String preGainOffset) {
284 ProfileContext context = mock(ProfileContext.class);
285 Configuration config = new Configuration();
287 config.put("gain", gain);
289 if (preGainOffset != null) {
290 config.put("pre-gain-offset", preGainOffset);
292 when(context.getConfiguration()).thenReturn(config);
294 return new ModbusGainOffsetProfile<>(callback, context);