2 * Copyright (c) 2010-2020 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.ExecutionException;
28 import java.util.concurrent.ScheduledExecutorService;
29 import java.util.concurrent.ScheduledThreadPoolExecutor;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.TimeoutException;
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.RawType;
54 import org.openhab.core.library.types.StringType;
55 import org.openhab.core.thing.ChannelUID;
58 * Tests the {@link ChannelState} class.
60 * @author David Graeff - Initial contribution
62 @ExtendWith(MockitoExtension.class)
63 @MockitoSettings(strictness = Strictness.WARN)
64 public class ChannelStateTests {
66 private @Mock MqttBrokerConnection connection;
67 private @Mock ChannelStateUpdateListener channelStateUpdateListener;
68 private @Mock ChannelUID channelUID;
69 private @Spy TextValue textValue;
71 private ScheduledExecutorService scheduler;
73 private ChannelConfig config = ChannelConfigBuilder.create("state", "command").build();
77 CompletableFuture<Void> voidFutureComplete = new CompletableFuture<>();
78 voidFutureComplete.complete(null);
79 doReturn(voidFutureComplete).when(connection).unsubscribeAll();
80 doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any());
81 doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any());
82 doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any());
83 doReturn(CompletableFuture.completedFuture(true)).when(connection).publish(any(), any(), anyInt(),
86 scheduler = new ScheduledThreadPoolExecutor(1);
90 public void tearDown() {
91 scheduler.shutdownNow();
95 public void noInteractionTimeoutTest() throws InterruptedException, ExecutionException, TimeoutException {
96 ChannelState c = spy(new ChannelState(config, channelUID, textValue, channelStateUpdateListener));
97 c.start(connection, scheduler, 50).get(100, TimeUnit.MILLISECONDS);
98 verify(connection).subscribe(eq("state"), eq(c));
100 verify(connection).unsubscribe(eq("state"), eq(c));
104 public void publishFormatTest() throws InterruptedException, ExecutionException, TimeoutException {
105 ChannelState c = spy(new ChannelState(config, channelUID, textValue, channelStateUpdateListener));
107 c.start(connection, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
108 verify(connection).subscribe(eq("state"), eq(c));
110 c.publishValue(new StringType("UPDATE")).get();
111 verify(connection).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE".getBytes())), anyInt(),
114 c.config.formatBeforePublish = "prefix%s";
115 c.publishValue(new StringType("UPDATE")).get();
116 verify(connection).publish(eq("command"), argThat(p -> Arrays.equals(p, "prefixUPDATE".getBytes())), anyInt(),
119 c.config.formatBeforePublish = "%1$s-%1$s";
120 c.publishValue(new StringType("UPDATE")).get();
121 verify(connection).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE-UPDATE".getBytes())), anyInt(),
124 c.config.formatBeforePublish = "%s";
125 c.config.retained = true;
126 c.publishValue(new StringType("UPDATE")).get();
127 verify(connection).publish(eq("command"), any(), anyInt(), eq(true));
130 verify(connection).unsubscribe(eq("state"), eq(c));
134 public void receiveWildcardTest() throws InterruptedException, ExecutionException, TimeoutException {
135 ChannelState c = spy(new ChannelState(ChannelConfigBuilder.create("state/+/topic", "command").build(),
136 channelUID, textValue, channelStateUpdateListener));
138 CompletableFuture<@Nullable Void> future = c.start(connection, scheduler, 100);
139 c.processMessage("state/bla/topic", "A TEST".getBytes());
140 future.get(300, TimeUnit.MILLISECONDS);
142 assertThat(textValue.getChannelState().toString(), is("A TEST"));
143 verify(channelStateUpdateListener).updateChannelState(eq(channelUID), any());
147 public void receiveStringTest() throws InterruptedException, ExecutionException, TimeoutException {
148 ChannelState c = spy(new ChannelState(config, channelUID, textValue, channelStateUpdateListener));
150 CompletableFuture<@Nullable Void> future = c.start(connection, scheduler, 100);
151 c.processMessage("state", "A TEST".getBytes());
152 future.get(300, TimeUnit.MILLISECONDS);
154 assertThat(textValue.getChannelState().toString(), is("A TEST"));
155 verify(channelStateUpdateListener).updateChannelState(eq(channelUID), any());
159 public void receiveDecimalTest() {
160 NumberValue value = new NumberValue(null, null, new BigDecimal(10), null);
161 ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
162 c.start(connection, mock(ScheduledExecutorService.class), 100);
164 c.processMessage("state", "15".getBytes());
165 assertThat(value.getChannelState().toString(), is("15"));
167 c.processMessage("state", "INCREASE".getBytes());
168 assertThat(value.getChannelState().toString(), is("25"));
170 c.processMessage("state", "DECREASE".getBytes());
171 assertThat(value.getChannelState().toString(), is("15"));
173 verify(channelStateUpdateListener, times(3)).updateChannelState(eq(channelUID), any());
177 public void receiveDecimalFractionalTest() {
178 NumberValue value = new NumberValue(null, null, new BigDecimal(10.5), null);
179 ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
180 c.start(connection, mock(ScheduledExecutorService.class), 100);
182 c.processMessage("state", "5.5".getBytes());
183 assertThat(value.getChannelState().toString(), is("5.5"));
185 c.processMessage("state", "INCREASE".getBytes());
186 assertThat(value.getChannelState().toString(), is("16.0"));
190 public void receivePercentageTest() {
191 PercentageValue value = new PercentageValue(new BigDecimal(-100), new BigDecimal(100), new BigDecimal(10), null,
193 ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
194 c.start(connection, mock(ScheduledExecutorService.class), 100);
196 c.processMessage("state", "-100".getBytes()); // 0%
197 assertThat(value.getChannelState().toString(), is("0"));
199 c.processMessage("state", "100".getBytes()); // 100%
200 assertThat(value.getChannelState().toString(), is("100"));
202 c.processMessage("state", "0".getBytes()); // 50%
203 assertThat(value.getChannelState().toString(), is("50"));
205 c.processMessage("state", "INCREASE".getBytes());
206 assertThat(value.getChannelState().toString(), is("55"));
207 assertThat(value.getMQTTpublishValue(null), is("10"));
208 assertThat(value.getMQTTpublishValue("%03.0f"), is("010"));
212 public void receiveRGBColorTest() {
213 ColorValue value = new ColorValue(ColorMode.RGB, "FON", "FOFF", 10);
214 ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
215 c.start(connection, mock(ScheduledExecutorService.class), 100);
217 c.processMessage("state", "ON".getBytes()); // Normal on state
218 assertThat(value.getChannelState().toString(), is("0,0,10"));
219 assertThat(value.getMQTTpublishValue(null), is("25,25,25"));
221 c.processMessage("state", "FOFF".getBytes()); // Custom off state
222 assertThat(value.getChannelState().toString(), is("0,0,0"));
223 assertThat(value.getMQTTpublishValue(null), is("0,0,0"));
225 c.processMessage("state", "10".getBytes()); // Brightness only
226 assertThat(value.getChannelState().toString(), is("0,0,10"));
227 assertThat(value.getMQTTpublishValue(null), is("25,25,25"));
229 HSBType t = HSBType.fromRGB(12, 18, 231);
231 c.processMessage("state", "12,18,231".getBytes());
232 assertThat(value.getChannelState(), is(t)); // HSB
233 // rgb -> hsv -> rgb is quite lossy
234 assertThat(value.getMQTTpublishValue(null), is("13,20,225"));
235 assertThat(value.getMQTTpublishValue("%3$d,%2$d,%1$d"), is("225,20,13"));
239 public void receiveHSBColorTest() {
240 ColorValue value = new ColorValue(ColorMode.HSB, "FON", "FOFF", 10);
241 ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
242 c.start(connection, mock(ScheduledExecutorService.class), 100);
244 c.processMessage("state", "ON".getBytes()); // Normal on state
245 assertThat(value.getChannelState().toString(), is("0,0,10"));
246 assertThat(value.getMQTTpublishValue(null), is("0,0,10"));
248 c.processMessage("state", "FOFF".getBytes()); // Custom off state
249 assertThat(value.getChannelState().toString(), is("0,0,0"));
250 assertThat(value.getMQTTpublishValue(null), is("0,0,0"));
252 c.processMessage("state", "10".getBytes()); // Brightness only
253 assertThat(value.getChannelState().toString(), is("0,0,10"));
254 assertThat(value.getMQTTpublishValue(null), is("0,0,10"));
256 c.processMessage("state", "12,18,100".getBytes());
257 assertThat(value.getChannelState().toString(), is("12,18,100"));
258 assertThat(value.getMQTTpublishValue(null), is("12,18,100"));
262 public void receiveXYYColorTest() {
263 ColorValue value = new ColorValue(ColorMode.XYY, "FON", "FOFF", 10);
264 ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
265 c.start(connection, mock(ScheduledExecutorService.class), 100);
267 c.processMessage("state", "ON".getBytes()); // Normal on state
268 assertThat(value.getChannelState().toString(), is("0,0,10"));
269 assertThat(value.getMQTTpublishValue(null), is("0.312716,0.329002,10.00"));
271 c.processMessage("state", "FOFF".getBytes()); // Custom off state
272 assertThat(value.getChannelState().toString(), is("0,0,0"));
273 assertThat(value.getMQTTpublishValue(null), is("0.312716,0.329002,0.00"));
275 c.processMessage("state", "10".getBytes()); // Brightness only
276 assertThat(value.getChannelState().toString(), is("0,0,10"));
277 assertThat(value.getMQTTpublishValue(null), is("0.312716,0.329002,10.00"));
279 HSBType t = HSBType.fromXY(0.3f, 0.6f);
281 c.processMessage("state", "0.3,0.6,100".getBytes());
282 assertThat(value.getChannelState(), is(t)); // HSB
283 assertThat(value.getMQTTpublishValue(null), is("0.300000,0.600000,100.00"));
284 assertThat(value.getMQTTpublishValue("%3$.1f,%2$.4f,%1$.4f"), is("100.0,0.6000,0.3000"));
288 public void receiveLocationTest() {
289 LocationValue value = new LocationValue();
290 ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
291 c.start(connection, mock(ScheduledExecutorService.class), 100);
293 c.processMessage("state", "46.833974, 7.108433".getBytes());
294 assertThat(value.getChannelState().toString(), is("46.833974,7.108433"));
295 assertThat(value.getMQTTpublishValue(null), is("46.833974,7.108433"));
299 public void receiveDateTimeTest() {
300 DateTimeValue value = new DateTimeValue();
301 ChannelState subject = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
302 subject.start(connection, mock(ScheduledExecutorService.class), 100);
304 ZonedDateTime zd = ZonedDateTime.now();
305 String datetime = zd.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
307 subject.processMessage("state", datetime.getBytes());
309 String channelState = value.getChannelState().toString();
310 assertTrue(channelState.startsWith(datetime),
311 "Expected '" + channelState + "' to start with '" + datetime + "'");
312 assertThat(value.getMQTTpublishValue(null), is(datetime));
316 public void receiveImageTest() {
317 ImageValue value = new ImageValue();
318 ChannelState c = spy(new ChannelState(config, channelUID, value, channelStateUpdateListener));
319 c.start(connection, mock(ScheduledExecutorService.class), 100);
321 byte[] payload = new byte[] { (byte) 0xFF, (byte) 0xD8, 0x01, 0x02, (byte) 0xFF, (byte) 0xD9 };
322 c.processMessage("state", payload);
323 assertThat(value.getChannelState(), is(instanceOf(RawType.class)));
324 assertThat(((RawType) value.getChannelState()).getMimeType(), is("image/jpeg"));