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.mqtt.generic;
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.assertTrue;
18 import static org.mockito.ArgumentMatchers.*;
19 import static org.mockito.ArgumentMatchers.any;
20 import static org.mockito.Mockito.*;
22 import java.math.BigDecimal;
23 import java.time.ZonedDateTime;
24 import java.time.format.DateTimeFormatter;
25 import java.util.Arrays;
26 import java.util.concurrent.CompletableFuture;
27 import java.util.concurrent.ScheduledExecutorService;
28 import java.util.concurrent.ScheduledThreadPoolExecutor;
29 import java.util.concurrent.TimeUnit;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.junit.jupiter.api.AfterEach;
34 import org.junit.jupiter.api.BeforeEach;
35 import org.junit.jupiter.api.Test;
36 import org.junit.jupiter.api.extension.ExtendWith;
37 import org.mockito.Mock;
38 import org.mockito.Spy;
39 import org.mockito.junit.jupiter.MockitoExtension;
40 import org.mockito.junit.jupiter.MockitoSettings;
41 import org.mockito.quality.Strictness;
42 import org.openhab.binding.mqtt.generic.mapping.ColorMode;
43 import org.openhab.binding.mqtt.generic.values.ColorValue;
44 import org.openhab.binding.mqtt.generic.values.DateTimeValue;
45 import org.openhab.binding.mqtt.generic.values.ImageValue;
46 import org.openhab.binding.mqtt.generic.values.LocationValue;
47 import org.openhab.binding.mqtt.generic.values.NumberValue;
48 import org.openhab.binding.mqtt.generic.values.PercentageValue;
49 import org.openhab.binding.mqtt.generic.values.TextValue;
50 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
51 import org.openhab.core.library.types.HSBType;
52 import org.openhab.core.library.types.RawType;
53 import org.openhab.core.library.types.StringType;
54 import org.openhab.core.library.unit.Units;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.types.Command;
59 * Tests the {@link ChannelState} class.
61 * @author David Graeff - Initial contribution
63 @ExtendWith(MockitoExtension.class)
64 @MockitoSettings(strictness = Strictness.LENIENT)
66 public class ChannelStateTests {
68 private @Mock @NonNullByDefault({}) MqttBrokerConnection connectionMock;
69 private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListenerMock;
70 private @Mock @NonNullByDefault({}) ChannelUID channelUIDMock;
71 private @Spy @NonNullByDefault({}) TextValue textValue;
73 private @NonNullByDefault({}) ScheduledExecutorService scheduler;
75 private ChannelConfig config = ChannelConfigBuilder.create("state", "command").build();
79 CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>();
80 voidFutureComplete.complete(null);
81 doReturn(voidFutureComplete).when(connectionMock).unsubscribeAll();
82 doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).subscribe(any(), any());
83 doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).unsubscribe(any(), any());
84 doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).publish(any(), any(), anyInt(),
87 scheduler = new ScheduledThreadPoolExecutor(1);
91 public void tearDown() {
92 scheduler.shutdownNow();
96 public void noInteractionTimeoutTest() throws Exception {
97 ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
98 c.start(connectionMock, scheduler, 50).get(100, TimeUnit.MILLISECONDS);
99 verify(connectionMock).subscribe(eq("state"), eq(c));
101 verify(connectionMock).unsubscribe(eq("state"), eq(c));
105 public void publishFormatTest() throws Exception {
106 ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
108 c.start(connectionMock, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
109 verify(connectionMock).subscribe(eq("state"), eq(c));
111 c.publishValue(new StringType("UPDATE")).get();
112 verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE".getBytes())), anyInt(),
115 c.config.formatBeforePublish = "prefix%s";
116 c.publishValue(new StringType("UPDATE")).get();
117 verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "prefixUPDATE".getBytes())),
118 anyInt(), eq(false));
120 c.config.formatBeforePublish = "%1$s-%1$s";
121 c.publishValue(new StringType("UPDATE")).get();
122 verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE-UPDATE".getBytes())),
123 anyInt(), eq(false));
125 c.config.formatBeforePublish = "%s";
126 c.config.retained = true;
127 c.publishValue(new StringType("UPDATE")).get();
128 verify(connectionMock).publish(eq("command"), any(), anyInt(), eq(true));
131 verify(connectionMock).unsubscribe(eq("state"), eq(c));
135 public void receiveWildcardTest() throws Exception {
136 ChannelState c = spy(new ChannelState(ChannelConfigBuilder.create("state/+/topic", "command").build(),
137 channelUIDMock, textValue, channelStateUpdateListenerMock));
139 CompletableFuture<@Nullable Void> future = c.start(connectionMock, scheduler, 100);
140 c.processMessage("state/bla/topic", "A TEST".getBytes());
141 future.get(300, TimeUnit.MILLISECONDS);
143 assertThat(textValue.getChannelState().toString(), is("A TEST"));
144 verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
148 public void receiveStringTest() throws Exception {
149 ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
151 CompletableFuture<@Nullable Void> future = c.start(connectionMock, scheduler, 100);
152 c.processMessage("state", "A TEST".getBytes());
153 future.get(300, TimeUnit.MILLISECONDS);
155 assertThat(textValue.getChannelState().toString(), is("A TEST"));
156 verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
160 public void receiveDecimalTest() {
161 NumberValue value = new NumberValue(null, null, new BigDecimal(10), null);
162 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
163 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
165 c.processMessage("state", "15".getBytes());
166 assertThat(value.getChannelState().toString(), is("15"));
168 c.processMessage("state", "INCREASE".getBytes());
169 assertThat(value.getChannelState().toString(), is("25"));
171 c.processMessage("state", "DECREASE".getBytes());
172 assertThat(value.getChannelState().toString(), is("15"));
174 verify(channelStateUpdateListenerMock, times(3)).updateChannelState(eq(channelUIDMock), any());
178 public void receiveDecimalFractionalTest() {
179 NumberValue value = new NumberValue(null, null, new BigDecimal(10.5), null);
180 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
181 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
183 c.processMessage("state", "5.5".getBytes());
184 assertThat(value.getChannelState().toString(), is("5.5"));
186 c.processMessage("state", "INCREASE".getBytes());
187 assertThat(value.getChannelState().toString(), is("16.0"));
191 public void receiveDecimalUnitTest() {
192 NumberValue value = new NumberValue(null, null, new BigDecimal(10), Units.WATT);
193 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
194 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
196 c.processMessage("state", "15".getBytes());
197 assertThat(value.getChannelState().toString(), is("15 W"));
199 c.processMessage("state", "INCREASE".getBytes());
200 assertThat(value.getChannelState().toString(), is("25 W"));
202 c.processMessage("state", "DECREASE".getBytes());
203 assertThat(value.getChannelState().toString(), is("15 W"));
205 verify(channelStateUpdateListenerMock, times(3)).updateChannelState(eq(channelUIDMock), any());
209 public void receiveDecimalAsPercentageUnitTest() {
210 NumberValue value = new NumberValue(null, null, new BigDecimal(10), Units.PERCENT);
211 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
212 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
214 c.processMessage("state", "63.7".getBytes());
215 assertThat(value.getChannelState().toString(), is("63.7 %"));
217 verify(channelStateUpdateListenerMock, times(1)).updateChannelState(eq(channelUIDMock), any());
221 public void receivePercentageTest() {
222 PercentageValue value = new PercentageValue(new BigDecimal(-100), new BigDecimal(100), new BigDecimal(10), null,
224 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
225 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
227 c.processMessage("state", "-100".getBytes()); // 0%
228 assertThat(value.getChannelState().toString(), is("0"));
230 c.processMessage("state", "100".getBytes()); // 100%
231 assertThat(value.getChannelState().toString(), is("100"));
233 c.processMessage("state", "0".getBytes()); // 50%
234 assertThat(value.getChannelState().toString(), is("50"));
236 c.processMessage("state", "INCREASE".getBytes());
237 assertThat(value.getChannelState().toString(), is("55"));
238 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("10"));
239 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), "%03.0f"), is("010"));
243 public void receiveRGBColorTest() {
244 ColorValue value = new ColorValue(ColorMode.RGB, "FON", "FOFF", 10);
245 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
246 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
248 c.processMessage("state", "ON".getBytes()); // Normal on state
249 assertThat(value.getChannelState().toString(), is("0,0,10"));
250 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("25,25,25"));
252 c.processMessage("state", "FOFF".getBytes()); // Custom off state
253 assertThat(value.getChannelState().toString(), is("0,0,0"));
254 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,0"));
256 c.processMessage("state", "10".getBytes()); // Brightness only
257 assertThat(value.getChannelState().toString(), is("0,0,10"));
258 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("25,25,25"));
260 HSBType t = HSBType.fromRGB(12, 18, 231);
262 c.processMessage("state", "12,18,231".getBytes());
263 assertThat(value.getChannelState(), is(t)); // HSB
264 // rgb -> hsv -> rgb is quite lossy
265 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("11,18,232"));
266 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), "%3$d,%2$d,%1$d"), is("232,18,11"));
270 public void receiveHSBColorTest() {
271 ColorValue value = new ColorValue(ColorMode.HSB, "FON", "FOFF", 10);
272 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
273 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
275 c.processMessage("state", "ON".getBytes()); // Normal on state
276 assertThat(value.getChannelState().toString(), is("0,0,10"));
277 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,10"));
279 c.processMessage("state", "FOFF".getBytes()); // Custom off state
280 assertThat(value.getChannelState().toString(), is("0,0,0"));
281 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,0"));
283 c.processMessage("state", "10".getBytes()); // Brightness only
284 assertThat(value.getChannelState().toString(), is("0,0,10"));
285 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,10"));
287 c.processMessage("state", "12,18,100".getBytes());
288 assertThat(value.getChannelState().toString(), is("12,18,100"));
289 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("12,18,100"));
293 public void receiveXYYColorTest() {
294 ColorValue value = new ColorValue(ColorMode.XYY, "FON", "FOFF", 10);
295 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
296 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
298 c.processMessage("state", "ON".getBytes()); // Normal on state
299 assertThat(value.getChannelState().toString(), is("0,0,10"));
300 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0.322700,0.329000,10.00"));
302 c.processMessage("state", "FOFF".getBytes()); // Custom off state
303 assertThat(value.getChannelState().toString(), is("0,0,0"));
304 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0.000000,0.000000,0.00"));
306 c.processMessage("state", "10".getBytes()); // Brightness only
307 assertThat(value.getChannelState().toString(), is("0,0,10"));
308 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0.322700,0.329000,10.00"));
310 HSBType t = HSBType.fromXY(0.3f, 0.6f);
312 c.processMessage("state", "0.3,0.6,100".getBytes());
313 assertThat(value.getChannelState(), is(t)); // HSB
314 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0.298700,0.601500,100.00"));
315 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), "%3$.1f,%2$.4f,%1$.4f"),
316 is("100.0,0.6015,0.2987"));
320 public void receiveLocationTest() {
321 LocationValue value = new LocationValue();
322 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
323 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
325 c.processMessage("state", "46.833974, 7.108433".getBytes());
326 assertThat(value.getChannelState().toString(), is("46.833974,7.108433"));
327 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("46.833974,7.108433"));
331 public void receiveDateTimeTest() {
332 DateTimeValue value = new DateTimeValue();
333 ChannelState subject = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
334 subject.start(connectionMock, mock(ScheduledExecutorService.class), 100);
336 ZonedDateTime zd = ZonedDateTime.now();
337 String datetime = zd.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
339 subject.processMessage("state", datetime.getBytes());
341 String channelState = value.getChannelState().toString();
342 assertTrue(channelState.startsWith(datetime),
343 "Expected '" + channelState + "' to start with '" + datetime + "'");
344 assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is(datetime));
348 public void receiveImageTest() {
349 ImageValue value = new ImageValue();
350 ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
351 c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
353 byte[] payload = { (byte) 0xFF, (byte) 0xD8, 0x01, 0x02, (byte) 0xFF, (byte) 0xD9 };
354 c.processMessage("state", payload);
355 assertThat(value.getChannelState(), is(instanceOf(RawType.class)));
356 assertThat(((RawType) value.getChannelState()).getMimeType(), is("image/jpeg"));