2 * Copyright (c) 2010-2024 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.mqtt.generic;
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.hamcrest.number.IsCloseTo.closeTo;
18 import static org.junit.jupiter.api.Assertions.assertTrue;
19 import static org.mockito.ArgumentMatchers.*;
20 import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.Mockito.*;
23 import java.math.BigDecimal;
24 import java.time.ZonedDateTime;
25 import java.time.format.DateTimeFormatter;
26 import java.util.Arrays;
27 import java.util.concurrent.CompletableFuture;
28 import java.util.concurrent.ScheduledExecutorService;
29 import java.util.concurrent.ScheduledThreadPoolExecutor;
30 import java.util.concurrent.TimeUnit;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.junit.jupiter.api.AfterEach;
35 import org.junit.jupiter.api.BeforeEach;
36 import org.junit.jupiter.api.Test;
37 import org.junit.jupiter.api.extension.ExtendWith;
38 import org.mockito.Mock;
39 import org.mockito.Spy;
40 import org.mockito.junit.jupiter.MockitoExtension;
41 import org.mockito.junit.jupiter.MockitoSettings;
42 import org.mockito.quality.Strictness;
43 import org.openhab.binding.mqtt.generic.mapping.ColorMode;
44 import org.openhab.binding.mqtt.generic.values.ColorValue;
45 import org.openhab.binding.mqtt.generic.values.DateTimeValue;
46 import org.openhab.binding.mqtt.generic.values.ImageValue;
47 import org.openhab.binding.mqtt.generic.values.LocationValue;
48 import org.openhab.binding.mqtt.generic.values.NumberValue;
49 import org.openhab.binding.mqtt.generic.values.PercentageValue;
50 import org.openhab.binding.mqtt.generic.values.TextValue;
51 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
52 import org.openhab.core.library.types.HSBType;
53 import org.openhab.core.library.types.PercentType;
54 import org.openhab.core.library.types.RawType;
55 import org.openhab.core.library.types.StopMoveType;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.library.unit.Units;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.util.ColorUtil;
63 * Tests the {@link ChannelState} class.
65 * @author David Graeff - Initial contribution
67 @ExtendWith(MockitoExtension.class)
68 @MockitoSettings(strictness = Strictness.LENIENT)
70 public class ChannelStateTests {
72 private @Mock @NonNullByDefault({}) MqttBrokerConnection connectionMock;
73 private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListenerMock;
74 private @Mock @NonNullByDefault({}) ChannelUID channelUIDMock;
75 private @Spy @NonNullByDefault({}) TextValue textValue;
77 private @NonNullByDefault({}) ScheduledExecutorService scheduler;
79 private ChannelConfig config = ChannelConfigBuilder.create("state", "command").build();
83 CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>();
84 voidFutureComplete.complete(null);
85 doReturn(voidFutureComplete).when(connectionMock).unsubscribeAll();
86 doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).subscribe(any(), any());
87 doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).unsubscribe(any(), any());
88 doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).publish(any(), any(), anyInt(),
91 scheduler = new ScheduledThreadPoolExecutor(1);
95 public void tearDown() {
96 scheduler.shutdownNow();
100 public void noInteractionTimeoutTest() throws Exception {
101 ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
102 c.start(connectionMock, scheduler, 50).get(100, TimeUnit.MILLISECONDS);
103 verify(connectionMock).subscribe(eq("state"), eq(c));
105 verify(connectionMock).unsubscribe(eq("state"), eq(c));
109 public void publishFormatTest() throws Exception {
110 ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
112 c.start(connectionMock, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
113 verify(connectionMock).subscribe(eq("state"), eq(c));
115 c.publishValue(new StringType("UPDATE")).get();
116 verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE".getBytes())), anyInt(),
119 c.config.formatBeforePublish = "prefix%s";
120 c.publishValue(new StringType("UPDATE")).get();
121 verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "prefixUPDATE".getBytes())),
122 anyInt(), eq(false));
124 c.config.formatBeforePublish = "%1$s-%1$s";
125 c.publishValue(new StringType("UPDATE")).get();
126 verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE-UPDATE".getBytes())),
127 anyInt(), eq(false));
129 c.config.formatBeforePublish = "%s";
130 c.config.retained = true;
131 c.publishValue(new StringType("UPDATE")).get();
132 verify(connectionMock).publish(eq("command"), any(), anyInt(), eq(true));
135 verify(connectionMock).unsubscribe(eq("state"), eq(c));
139 public void publishStopTest() throws Exception {
140 ChannelConfig config = ChannelConfigBuilder.create("state", "command").build();
141 config.stop = "STOP";
142 ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
144 c.start(connectionMock, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
145 verify(connectionMock).subscribe(eq("state"), eq(c));
147 c.publishValue(StopMoveType.STOP).get();
148 verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "STOP".getBytes())), anyInt(),
152 verify(connectionMock).unsubscribe(eq("state"), eq(c));
156 public void publishStopSeparateTopicTest() throws Exception {
157 ChannelConfig config = ChannelConfigBuilder.create("state", "command").withStopCommandTopic("stopCommand")
159 config.stop = "STOP";
160 ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
162 c.start(connectionMock, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
163 verify(connectionMock).subscribe(eq("state"), eq(c));
165 c.publishValue(StopMoveType.STOP).get();
166 verify(connectionMock).publish(eq("stopCommand"), argThat(p -> Arrays.equals(p, "STOP".getBytes())), anyInt(),
170 verify(connectionMock).unsubscribe(eq("state"), eq(c));
174 public void receiveWildcardTest() throws Exception {
175 ChannelState c = spy(new ChannelState(ChannelConfigBuilder.create("state/+/topic", "command").build(),
176 channelUIDMock, textValue, channelStateUpdateListenerMock));
178 CompletableFuture<@Nullable Void> future = c.start(connectionMock, scheduler, 100);
179 c.processMessage("state/bla/topic", "A TEST".getBytes());
180 future.get(300, TimeUnit.MILLISECONDS);
182 assertThat(textValue.getChannelState().toString(), is("A TEST"));
183 verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
187 public void receiveStringTest() throws Exception {
188 ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
190 CompletableFuture<@Nullable Void> future = c.start(connectionMock, scheduler, 100);
191 c.processMessage("state", "A TEST".getBytes());
192 future.get(300, TimeUnit.MILLISECONDS);
194 assertThat(textValue.getChannelState().toString(), is("A TEST"));
195 verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
199 public void receiveDecimalTest() {
200 NumberValue value = new NumberValue(null, null, new BigDecimal(10), null);
201 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
202 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
204 c.processMessage("state", "15".getBytes());
205 assertThat(value.getChannelState().toString(), is("15"));
207 c.processMessage("state", "INCREASE".getBytes());
208 assertThat(value.getChannelState().toString(), is("25"));
210 c.processMessage("state", "DECREASE".getBytes());
211 assertThat(value.getChannelState().toString(), is("15"));
213 verify(channelStateUpdateListenerMock, times(3)).updateChannelState(eq(channelUIDMock), any());
217 public void receiveDecimalFractionalTest() {
218 NumberValue value = new NumberValue(null, null, new BigDecimal(10.5), null);
219 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
220 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
222 c.processMessage("state", "5.5".getBytes());
223 assertThat(value.getChannelState().toString(), is("5.5"));
225 c.processMessage("state", "INCREASE".getBytes());
226 assertThat(value.getChannelState().toString(), is("16.0"));
230 public void receiveDecimalUnitTest() {
231 NumberValue value = new NumberValue(null, null, new BigDecimal(10), Units.WATT);
232 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
233 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
235 c.processMessage("state", "15".getBytes());
236 assertThat(value.getChannelState().toString(), is("15 W"));
238 c.processMessage("state", "INCREASE".getBytes());
239 assertThat(value.getChannelState().toString(), is("25 W"));
241 c.processMessage("state", "DECREASE".getBytes());
242 assertThat(value.getChannelState().toString(), is("15 W"));
244 verify(channelStateUpdateListenerMock, times(3)).updateChannelState(eq(channelUIDMock), any());
248 public void receiveDecimalAsPercentageUnitTest() {
249 NumberValue value = new NumberValue(null, null, new BigDecimal(10), Units.PERCENT);
250 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
251 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
253 c.processMessage("state", "63.7".getBytes());
254 assertThat(value.getChannelState().toString(), is("63.7 %"));
256 verify(channelStateUpdateListenerMock, times(1)).updateChannelState(eq(channelUIDMock), any());
260 public void receivePercentageTest() {
261 PercentageValue value = new PercentageValue(new BigDecimal(-100), new BigDecimal(100), new BigDecimal(10), null,
263 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
264 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
266 c.processMessage("state", "-100".getBytes()); // 0%
267 assertThat(value.getChannelState().toString(), is("0"));
269 c.processMessage("state", "100".getBytes()); // 100%
270 assertThat(value.getChannelState().toString(), is("100"));
272 c.processMessage("state", "0".getBytes()); // 50%
273 assertThat(value.getChannelState().toString(), is("50"));
275 c.processMessage("state", "INCREASE".getBytes());
276 assertThat(value.getChannelState().toString(), is("55"));
277 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("10"));
278 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), "%03.0f"), is("010"));
282 public void receiveRGBColorTest() {
283 ColorValue value = new ColorValue(ColorMode.RGB, "FON", "FOFF", 10);
284 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
285 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
287 c.processMessage("state", "ON".getBytes()); // Normal on state
288 assertThat(value.getChannelState().toString(), is("0,0,10"));
289 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("26,26,26"));
291 c.processMessage("state", "FOFF".getBytes()); // Custom off state
292 assertThat(value.getChannelState().toString(), is("0,0,0"));
293 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,0"));
295 c.processMessage("state", "10".getBytes()); // Brightness only
296 assertThat(value.getChannelState().toString(), is("0,0,10"));
297 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("26,26,26"));
299 HSBType t = HSBType.fromRGB(12, 18, 231);
301 c.processMessage("state", "12,18,231".getBytes());
302 assertThat(value.getChannelState(), is(t)); // HSB
303 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("12,18,231"));
304 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), "%3$d,%2$d,%1$d"), is("231,18,12"));
308 public void receiveHSBColorTest() {
309 ColorValue value = new ColorValue(ColorMode.HSB, "FON", "FOFF", 10);
310 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
311 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
313 c.processMessage("state", "ON".getBytes()); // Normal on state
314 assertThat(value.getChannelState().toString(), is("0,0,10"));
315 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,10"));
317 c.processMessage("state", "FOFF".getBytes()); // Custom off state
318 assertThat(value.getChannelState().toString(), is("0,0,0"));
319 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,0"));
321 c.processMessage("state", "10".getBytes()); // Brightness only
322 assertThat(value.getChannelState().toString(), is("0,0,10"));
323 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,10"));
325 c.processMessage("state", "12,18,100".getBytes());
326 assertThat(value.getChannelState().toString(), is("12,18,100"));
327 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("12,18,100"));
331 public void receiveXYYColorTest() {
332 ColorValue value = new ColorValue(ColorMode.XYY, "FON", "FOFF", 10);
333 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
334 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
337 c.processMessage("state", "ON".getBytes()); // Normal on state
338 assertThat(value.getChannelState().toString(), is("0,0,10"));
340 c.processMessage("state", "FOFF".getBytes()); // Custom off state
341 // note we don't care what color value is currently stored, just that brightness is off
342 assertThat(((HSBType) value.getChannelState()).getBrightness(), is(PercentType.ZERO));
344 c.processMessage("state", "10".getBytes()); // Brightness only
345 assertThat(value.getChannelState().toString(), is("0,0,10"));
347 HSBType t = ColorUtil.xyToHsb(new double[] { 0.3f, 0.6f });
348 c.processMessage("state", "0.3,0.6,100".getBytes());
349 assertTrue(((HSBType) value.getChannelState()).closeTo(t, 0.001)); // HSB
352 // these use the 0.3,0.6,100 from above, but care more about proper formatting of the outgoing message
353 // than about the precise value (since color conversions have happened)
354 assertCloseTo(value.getMQTTpublishValue((Command) value.getChannelState(), null), "0.300000,0.600000,100.00");
355 assertCloseTo(value.getMQTTpublishValue((Command) value.getChannelState(), "%3$.1f,%2$.2f,%1$.2f"),
359 // also ensures the string elements are the same _length_, i.e. the correct precision for each element
360 private void assertCloseTo(String aString, String bString) {
361 String[] aElements = aString.split(",");
362 String[] bElements = bString.split(",");
363 double[] a = Arrays.stream(aElements).mapToDouble(Double::parseDouble).toArray();
364 double[] b = Arrays.stream(bElements).mapToDouble(Double::parseDouble).toArray();
365 for (int i = 0; i < a.length; i++) {
366 assertThat(aElements[i].length(), is(bElements[i].length()));
367 assertThat(a[i], closeTo(b[i], 0.002));
372 public void receiveLocationTest() {
373 LocationValue value = new LocationValue();
374 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
375 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
377 c.processMessage("state", "46.833974, 7.108433".getBytes());
378 assertThat(value.getChannelState().toString(), is("46.833974,7.108433"));
379 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("46.833974,7.108433"));
383 public void receiveDateTimeTest() {
384 DateTimeValue value = new DateTimeValue();
385 ChannelState subject = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
386 subject.start(connectionMock, mock(ScheduledExecutorService.class), 100);
388 ZonedDateTime zd = ZonedDateTime.now();
389 String datetime = zd.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
391 subject.processMessage("state", datetime.getBytes());
393 String channelState = value.getChannelState().toString();
394 assertTrue(channelState.startsWith(datetime),
395 "Expected '" + channelState + "' to start with '" + datetime + "'");
396 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is(datetime));
400 public void receiveImageTest() {
401 ImageValue value = new ImageValue();
402 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
403 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
405 byte[] payload = { (byte) 0xFF, (byte) 0xD8, 0x01, 0x02, (byte) 0xFF, (byte) 0xD9 };
406 c.processMessage("state", payload);
407 assertThat(value.getChannelState(), is(instanceOf(RawType.class)));
408 assertThat(((RawType) value.getChannelState()).getMimeType(), is("image/jpeg"));