]> git.basschouten.com Git - openhab-addons.git/blob
018abe734b51be47981ea13cb5e45b77b8783b78
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mqtt.generic;
14
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.*;
22
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;
31
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.StringType;
56 import org.openhab.core.library.unit.Units;
57 import org.openhab.core.thing.ChannelUID;
58 import org.openhab.core.types.Command;
59 import org.openhab.core.util.ColorUtil;
60
61 /**
62  * Tests the {@link ChannelState} class.
63  *
64  * @author David Graeff - Initial contribution
65  */
66 @ExtendWith(MockitoExtension.class)
67 @MockitoSettings(strictness = Strictness.LENIENT)
68 @NonNullByDefault
69 public class ChannelStateTests {
70
71     private @Mock @NonNullByDefault({}) MqttBrokerConnection connectionMock;
72     private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListenerMock;
73     private @Mock @NonNullByDefault({}) ChannelUID channelUIDMock;
74     private @Spy @NonNullByDefault({}) TextValue textValue;
75
76     private @NonNullByDefault({}) ScheduledExecutorService scheduler;
77
78     private ChannelConfig config = ChannelConfigBuilder.create("state", "command").build();
79
80     @BeforeEach
81     public void setUp() {
82         CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>();
83         voidFutureComplete.complete(null);
84         doReturn(voidFutureComplete).when(connectionMock).unsubscribeAll();
85         doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).subscribe(any(), any());
86         doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).unsubscribe(any(), any());
87         doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).publish(any(), any(), anyInt(),
88                 anyBoolean());
89
90         scheduler = new ScheduledThreadPoolExecutor(1);
91     }
92
93     @AfterEach
94     public void tearDown() {
95         scheduler.shutdownNow();
96     }
97
98     @Test
99     public void noInteractionTimeoutTest() throws Exception {
100         ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
101         c.start(connectionMock, scheduler, 50).get(100, TimeUnit.MILLISECONDS);
102         verify(connectionMock).subscribe(eq("state"), eq(c));
103         c.stop().get();
104         verify(connectionMock).unsubscribe(eq("state"), eq(c));
105     }
106
107     @Test
108     public void publishFormatTest() throws Exception {
109         ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
110
111         c.start(connectionMock, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
112         verify(connectionMock).subscribe(eq("state"), eq(c));
113
114         c.publishValue(new StringType("UPDATE")).get();
115         verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE".getBytes())), anyInt(),
116                 eq(false));
117
118         c.config.formatBeforePublish = "prefix%s";
119         c.publishValue(new StringType("UPDATE")).get();
120         verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "prefixUPDATE".getBytes())),
121                 anyInt(), eq(false));
122
123         c.config.formatBeforePublish = "%1$s-%1$s";
124         c.publishValue(new StringType("UPDATE")).get();
125         verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE-UPDATE".getBytes())),
126                 anyInt(), eq(false));
127
128         c.config.formatBeforePublish = "%s";
129         c.config.retained = true;
130         c.publishValue(new StringType("UPDATE")).get();
131         verify(connectionMock).publish(eq("command"), any(), anyInt(), eq(true));
132
133         c.stop().get();
134         verify(connectionMock).unsubscribe(eq("state"), eq(c));
135     }
136
137     @Test
138     public void receiveWildcardTest() throws Exception {
139         ChannelState c = spy(new ChannelState(ChannelConfigBuilder.create("state/+/topic", "command").build(),
140                 channelUIDMock, textValue, channelStateUpdateListenerMock));
141
142         CompletableFuture<@Nullable Void> future = c.start(connectionMock, scheduler, 100);
143         c.processMessage("state/bla/topic", "A TEST".getBytes());
144         future.get(300, TimeUnit.MILLISECONDS);
145
146         assertThat(textValue.getChannelState().toString(), is("A TEST"));
147         verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
148     }
149
150     @Test
151     public void receiveStringTest() throws Exception {
152         ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
153
154         CompletableFuture<@Nullable Void> future = c.start(connectionMock, scheduler, 100);
155         c.processMessage("state", "A TEST".getBytes());
156         future.get(300, TimeUnit.MILLISECONDS);
157
158         assertThat(textValue.getChannelState().toString(), is("A TEST"));
159         verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
160     }
161
162     @Test
163     public void receiveDecimalTest() {
164         NumberValue value = new NumberValue(null, null, new BigDecimal(10), null);
165         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
166         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
167
168         c.processMessage("state", "15".getBytes());
169         assertThat(value.getChannelState().toString(), is("15"));
170
171         c.processMessage("state", "INCREASE".getBytes());
172         assertThat(value.getChannelState().toString(), is("25"));
173
174         c.processMessage("state", "DECREASE".getBytes());
175         assertThat(value.getChannelState().toString(), is("15"));
176
177         verify(channelStateUpdateListenerMock, times(3)).updateChannelState(eq(channelUIDMock), any());
178     }
179
180     @Test
181     public void receiveDecimalFractionalTest() {
182         NumberValue value = new NumberValue(null, null, new BigDecimal(10.5), null);
183         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
184         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
185
186         c.processMessage("state", "5.5".getBytes());
187         assertThat(value.getChannelState().toString(), is("5.5"));
188
189         c.processMessage("state", "INCREASE".getBytes());
190         assertThat(value.getChannelState().toString(), is("16.0"));
191     }
192
193     @Test
194     public void receiveDecimalUnitTest() {
195         NumberValue value = new NumberValue(null, null, new BigDecimal(10), Units.WATT);
196         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
197         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
198
199         c.processMessage("state", "15".getBytes());
200         assertThat(value.getChannelState().toString(), is("15 W"));
201
202         c.processMessage("state", "INCREASE".getBytes());
203         assertThat(value.getChannelState().toString(), is("25 W"));
204
205         c.processMessage("state", "DECREASE".getBytes());
206         assertThat(value.getChannelState().toString(), is("15 W"));
207
208         verify(channelStateUpdateListenerMock, times(3)).updateChannelState(eq(channelUIDMock), any());
209     }
210
211     @Test
212     public void receiveDecimalAsPercentageUnitTest() {
213         NumberValue value = new NumberValue(null, null, new BigDecimal(10), Units.PERCENT);
214         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
215         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
216
217         c.processMessage("state", "63.7".getBytes());
218         assertThat(value.getChannelState().toString(), is("63.7 %"));
219
220         verify(channelStateUpdateListenerMock, times(1)).updateChannelState(eq(channelUIDMock), any());
221     }
222
223     @Test
224     public void receivePercentageTest() {
225         PercentageValue value = new PercentageValue(new BigDecimal(-100), new BigDecimal(100), new BigDecimal(10), null,
226                 null);
227         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
228         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
229
230         c.processMessage("state", "-100".getBytes()); // 0%
231         assertThat(value.getChannelState().toString(), is("0"));
232
233         c.processMessage("state", "100".getBytes()); // 100%
234         assertThat(value.getChannelState().toString(), is("100"));
235
236         c.processMessage("state", "0".getBytes()); // 50%
237         assertThat(value.getChannelState().toString(), is("50"));
238
239         c.processMessage("state", "INCREASE".getBytes());
240         assertThat(value.getChannelState().toString(), is("55"));
241         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("10"));
242         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), "%03.0f"), is("010"));
243     }
244
245     @Test
246     public void receiveRGBColorTest() {
247         ColorValue value = new ColorValue(ColorMode.RGB, "FON", "FOFF", 10);
248         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
249         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
250
251         c.processMessage("state", "ON".getBytes()); // Normal on state
252         assertThat(value.getChannelState().toString(), is("0,0,10"));
253         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("26,26,26"));
254
255         c.processMessage("state", "FOFF".getBytes()); // Custom off state
256         assertThat(value.getChannelState().toString(), is("0,0,0"));
257         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,0"));
258
259         c.processMessage("state", "10".getBytes()); // Brightness only
260         assertThat(value.getChannelState().toString(), is("0,0,10"));
261         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("26,26,26"));
262
263         HSBType t = HSBType.fromRGB(12, 18, 231);
264
265         c.processMessage("state", "12,18,231".getBytes());
266         assertThat(value.getChannelState(), is(t)); // HSB
267         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("12,18,231"));
268         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), "%3$d,%2$d,%1$d"), is("231,18,12"));
269     }
270
271     @Test
272     public void receiveHSBColorTest() {
273         ColorValue value = new ColorValue(ColorMode.HSB, "FON", "FOFF", 10);
274         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
275         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
276
277         c.processMessage("state", "ON".getBytes()); // Normal on state
278         assertThat(value.getChannelState().toString(), is("0,0,10"));
279         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,10"));
280
281         c.processMessage("state", "FOFF".getBytes()); // Custom off state
282         assertThat(value.getChannelState().toString(), is("0,0,0"));
283         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,0"));
284
285         c.processMessage("state", "10".getBytes()); // Brightness only
286         assertThat(value.getChannelState().toString(), is("0,0,10"));
287         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("0,0,10"));
288
289         c.processMessage("state", "12,18,100".getBytes());
290         assertThat(value.getChannelState().toString(), is("12,18,100"));
291         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("12,18,100"));
292     }
293
294     @Test
295     public void receiveXYYColorTest() {
296         ColorValue value = new ColorValue(ColorMode.XYY, "FON", "FOFF", 10);
297         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
298         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
299
300         // incoming messages
301         c.processMessage("state", "ON".getBytes()); // Normal on state
302         assertThat(value.getChannelState().toString(), is("0,0,10"));
303
304         c.processMessage("state", "FOFF".getBytes()); // Custom off state
305         // note we don't care what color value is currently stored, just that brightness is off
306         assertThat(((HSBType) value.getChannelState()).getBrightness(), is(PercentType.ZERO));
307
308         c.processMessage("state", "10".getBytes()); // Brightness only
309         assertThat(value.getChannelState().toString(), is("0,0,10"));
310
311         HSBType t = ColorUtil.xyToHsb(new double[] { 0.3f, 0.6f });
312         c.processMessage("state", "0.3,0.6,100".getBytes());
313         assertTrue(((HSBType) value.getChannelState()).closeTo(t, 0.001)); // HSB
314
315         // outgoing messages
316         // these use the 0.3,0.6,100 from above, but care more about proper formatting of the outgoing message
317         // than about the precise value (since color conversions have happened)
318         assertCloseTo(value.getMQTTpublishValue((Command) value.getChannelState(), null), "0.300000,0.600000,100.00");
319         assertCloseTo(value.getMQTTpublishValue((Command) value.getChannelState(), "%3$.1f,%2$.2f,%1$.2f"),
320                 "100.0,0.60,0.30");
321     }
322
323     // also ensures the string elements are the same _length_, i.e. the correct precision for each element
324     private void assertCloseTo(String aString, String bString) {
325         String[] aElements = aString.split(",");
326         String[] bElements = bString.split(",");
327         double[] a = Arrays.stream(aElements).mapToDouble(Double::parseDouble).toArray();
328         double[] b = Arrays.stream(bElements).mapToDouble(Double::parseDouble).toArray();
329         for (int i = 0; i < a.length; i++) {
330             assertThat(aElements[i].length(), is(bElements[i].length()));
331             assertThat(a[i], closeTo(b[i], 0.002));
332         }
333     }
334
335     @Test
336     public void receiveLocationTest() {
337         LocationValue value = new LocationValue();
338         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
339         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
340
341         c.processMessage("state", "46.833974, 7.108433".getBytes());
342         assertThat(value.getChannelState().toString(), is("46.833974,7.108433"));
343         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is("46.833974,7.108433"));
344     }
345
346     @Test
347     public void receiveDateTimeTest() {
348         DateTimeValue value = new DateTimeValue();
349         ChannelState subject = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
350         subject.start(connectionMock, mock(ScheduledExecutorService.class), 100);
351
352         ZonedDateTime zd = ZonedDateTime.now();
353         String datetime = zd.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
354
355         subject.processMessage("state", datetime.getBytes());
356
357         String channelState = value.getChannelState().toString();
358         assertTrue(channelState.startsWith(datetime),
359                 "Expected '" + channelState + "' to start with '" + datetime + "'");
360         assertThat(value.getMQTTpublishValue((Command) value.getChannelState(), null), is(datetime));
361     }
362
363     @Test
364     public void receiveImageTest() {
365         ImageValue value = new ImageValue();
366         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
367         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
368
369         byte[] payload = { (byte) 0xFF, (byte) 0xD8, 0x01, 0x02, (byte) 0xFF, (byte) 0xD9 };
370         c.processMessage("state", payload);
371         assertThat(value.getChannelState(), is(instanceOf(RawType.class)));
372         assertThat(((RawType) value.getChannelState()).getMimeType(), is("image/jpeg"));
373     }
374 }