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) {
141 updateFromHandler = new QuantityType((String) updateFromHandlerObj);
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) {
155 updateFromHandler = new QuantityType((String) updateFromHandlerObj);
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)
169 ? new QuantityType((String) expectedUpdateTowardsItemObj)
170 : (Type) expectedUpdateTowardsItemObj;
171 assertEquals(expectedStateUpdateTowardsItem, actualStateUpdateTowardsItem);
172 verifyNoMoreInteractions(callback);
175 static Stream<Arguments> provideAdditionalArgsForCommandFromItem() {
177 // Dimensionless conversion 2.5/1% = 250%/1% = 250
178 // gain in %, command as bare ratio and the other way around
179 Arguments.of("0", "1 %", "250", "2.5"), Arguments.of("2%", "1 %", "249.9800", "2.5"),
181 // celsius gain, kelvin command
182 Arguments.of("0", "0.1 °C", "-2706.5", "2.5 K"),
184 // incompatible command unit, should be convertible with gain
185 Arguments.of("0", "0.1 °C", null, "2.5 m/s"),
187 // incompatible offset unit
189 Arguments.of("50 K", "21", null, "30 m/s"), Arguments.of("50 m/s", "21", null, "30 K"),
191 // UNDEF command is not processed
193 Arguments.of("0", "0", null, UnDefType.UNDEF),
195 // REFRESH command is forwarded
197 Arguments.of("0", "0", RefreshType.REFRESH, RefreshType.REFRESH)
204 * Test profile behavior when item receives command
206 * @param preGainOffset profile pre-gain-offset
207 * @param gain profile gain
208 * @param expectedCommandTowardsHandlerObj expected command towards handler. String representing QuantityType or
209 * Command. Use null to verify that no commands are sent to handler.
210 * @param commandFromItemObj command that item receives. String representing QuantityType or Command.
212 @SuppressWarnings({ "rawtypes" })
214 @MethodSource({ "provideArgsForBoth", "provideAdditionalArgsForCommandFromItem" })
215 public void testOnCommandFromItem(String preGainOffset, String gain,
216 @Nullable Object expectedCommandTowardsHandlerObj, Object commandFromItemObj) {
217 assumeFalse(commandFromItemObj.equals(UnDefType.UNDEF));
218 ProfileCallback callback = mock(ProfileCallback.class);
219 ModbusGainOffsetProfile profile = createProfile(callback, gain, preGainOffset);
221 Command commandFromItem = (commandFromItemObj instanceof String) ? new QuantityType((String) commandFromItemObj)
222 : (Command) commandFromItemObj;
223 profile.onCommandFromItem(commandFromItem);
225 boolean callsExpected = expectedCommandTowardsHandlerObj != null;
227 ArgumentCaptor<Command> capture = ArgumentCaptor.forClass(Command.class);
228 verify(callback, times(1)).handleCommand(capture.capture());
229 Command actualCommandTowardsHandler = capture.getValue();
230 Command expectedCommandTowardsHandler = (expectedCommandTowardsHandlerObj instanceof String)
231 ? new QuantityType((String) expectedCommandTowardsHandlerObj)
232 : (Command) expectedCommandTowardsHandlerObj;
233 assertEquals(expectedCommandTowardsHandler, actualCommandTowardsHandler);
234 verifyNoMoreInteractions(callback);
236 verifyNoInteractions(callback);
242 * Test behaviour when item receives state update from item (no-op)
246 public void testOnCommandFromItem() {
247 ProfileCallback callback = mock(ProfileCallback.class);
248 ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0");
250 profile.onStateUpdateFromItem(new DecimalType(3.78));
252 verifyNoInteractions(callback);
256 public void testInvalidInit() {
257 // preGainOffset must be dimensionless
258 ProfileCallback callback = mock(ProfileCallback.class);
259 ModbusGainOffsetProfile<?> profile = createProfile(callback, "1.0", "0.0 K");
260 assertFalse(profile.isValid());
266 public void testInitGainDefault(String gain) {
267 ProfileCallback callback = mock(ProfileCallback.class);
268 ModbusGainOffsetProfile<?> p = createProfile(callback, gain, "0.0");
269 assertTrue(p.isValid());
270 assertEquals(p.getGain(), Optional.of(QuantityType.ONE));
276 public void testInitOffsetDefault(String preGainOffset) {
277 ProfileCallback callback = mock(ProfileCallback.class);
278 ModbusGainOffsetProfile<?> p = createProfile(callback, "1", preGainOffset);
279 assertTrue(p.isValid());
280 assertEquals(p.getPregainOffset(), Optional.of(QuantityType.ZERO));
283 private ModbusGainOffsetProfile<?> createProfile(ProfileCallback callback, @Nullable String gain,
284 @Nullable String preGainOffset) {
285 ProfileContext context = mock(ProfileContext.class);
286 Configuration config = new Configuration();
288 config.put("gain", gain);
290 if (preGainOffset != null) {
291 config.put("pre-gain-offset", preGainOffset);
293 when(context.getConfiguration()).thenReturn(config);
295 return new ModbusGainOffsetProfile<>(callback, context);