]> git.basschouten.com Git - openhab-addons.git/blob
69c96ea2e969b4001e98f8f19477db48903c8a0f
[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.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;
61
62 /**
63  * Tests the {@link ChannelState} class.
64  *
65  * @author David Graeff - Initial contribution
66  */
67 @ExtendWith(MockitoExtension.class)
68 @MockitoSettings(strictness = Strictness.LENIENT)
69 @NonNullByDefault
70 public class ChannelStateTests {
71
72     private @Mock @NonNullByDefault({}) MqttBrokerConnection connectionMock;
73     private @Mock @NonNullByDefault({}) ChannelStateUpdateListener channelStateUpdateListenerMock;
74     private @Mock @NonNullByDefault({}) ChannelUID channelUIDMock;
75     private @Spy @NonNullByDefault({}) TextValue textValue;
76
77     private @NonNullByDefault({}) ScheduledExecutorService scheduler;
78
79     private ChannelConfig config = ChannelConfigBuilder.create("state", "command").build();
80
81     @BeforeEach
82     public void setUp() {
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(),
89                 anyBoolean());
90
91         scheduler = new ScheduledThreadPoolExecutor(1);
92     }
93
94     @AfterEach
95     public void tearDown() {
96         scheduler.shutdownNow();
97     }
98
99     @Test
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));
104         c.stop().get();
105         verify(connectionMock).unsubscribe(eq("state"), eq(c));
106     }
107
108     @Test
109     public void publishFormatTest() throws Exception {
110         ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
111
112         c.start(connectionMock, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
113         verify(connectionMock).subscribe(eq("state"), eq(c));
114
115         c.publishValue(new StringType("UPDATE")).get();
116         verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "UPDATE".getBytes())), anyInt(),
117                 eq(false));
118
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));
123
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));
128
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));
133
134         c.stop().get();
135         verify(connectionMock).unsubscribe(eq("state"), eq(c));
136     }
137
138     @Test
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));
143
144         c.start(connectionMock, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
145         verify(connectionMock).subscribe(eq("state"), eq(c));
146
147         c.publishValue(StopMoveType.STOP).get();
148         verify(connectionMock).publish(eq("command"), argThat(p -> Arrays.equals(p, "STOP".getBytes())), anyInt(),
149                 eq(false));
150
151         c.stop().get();
152         verify(connectionMock).unsubscribe(eq("state"), eq(c));
153     }
154
155     @Test
156     public void publishStopSeparateTopicTest() throws Exception {
157         ChannelConfig config = ChannelConfigBuilder.create("state", "command").withStopCommandTopic("stopCommand")
158                 .build();
159         config.stop = "STOP";
160         ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
161
162         c.start(connectionMock, scheduler, 0).get(50, TimeUnit.MILLISECONDS);
163         verify(connectionMock).subscribe(eq("state"), eq(c));
164
165         c.publishValue(StopMoveType.STOP).get();
166         verify(connectionMock).publish(eq("stopCommand"), argThat(p -> Arrays.equals(p, "STOP".getBytes())), anyInt(),
167                 eq(false));
168
169         c.stop().get();
170         verify(connectionMock).unsubscribe(eq("state"), eq(c));
171     }
172
173     @Test
174     public void receiveWildcardTest() throws Exception {
175         ChannelState c = spy(new ChannelState(ChannelConfigBuilder.create("state/+/topic", "command").build(),
176                 channelUIDMock, textValue, channelStateUpdateListenerMock));
177
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);
181
182         assertThat(textValue.getChannelState().toString(), is("A TEST"));
183         verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
184     }
185
186     @Test
187     public void receiveStringTest() throws Exception {
188         ChannelState c = spy(new ChannelState(config, channelUIDMock, textValue, channelStateUpdateListenerMock));
189
190         CompletableFuture<@Nullable Void> future = c.start(connectionMock, scheduler, 100);
191         c.processMessage("state", "A TEST".getBytes());
192         future.get(300, TimeUnit.MILLISECONDS);
193
194         assertThat(textValue.getChannelState().toString(), is("A TEST"));
195         verify(channelStateUpdateListenerMock).updateChannelState(eq(channelUIDMock), any());
196     }
197
198     @Test
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);
203
204         c.processMessage("state", "15".getBytes());
205         assertThat(value.getChannelState().toString(), is("15"));
206
207         c.processMessage("state", "INCREASE".getBytes());
208         assertThat(value.getChannelState().toString(), is("25"));
209
210         c.processMessage("state", "DECREASE".getBytes());
211         assertThat(value.getChannelState().toString(), is("15"));
212
213         verify(channelStateUpdateListenerMock, times(3)).updateChannelState(eq(channelUIDMock), any());
214     }
215
216     @Test
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);
221
222         c.processMessage("state", "5.5".getBytes());
223         assertThat(value.getChannelState().toString(), is("5.5"));
224
225         c.processMessage("state", "INCREASE".getBytes());
226         assertThat(value.getChannelState().toString(), is("16.0"));
227     }
228
229     @Test
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);
234
235         c.processMessage("state", "15".getBytes());
236         assertThat(value.getChannelState().toString(), is("15 W"));
237
238         c.processMessage("state", "INCREASE".getBytes());
239         assertThat(value.getChannelState().toString(), is("25 W"));
240
241         c.processMessage("state", "DECREASE".getBytes());
242         assertThat(value.getChannelState().toString(), is("15 W"));
243
244         verify(channelStateUpdateListenerMock, times(3)).updateChannelState(eq(channelUIDMock), any());
245     }
246
247     @Test
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);
252
253         c.processMessage("state", "63.7".getBytes());
254         assertThat(value.getChannelState().toString(), is("63.7 %"));
255
256         verify(channelStateUpdateListenerMock, times(1)).updateChannelState(eq(channelUIDMock), any());
257     }
258
259     @Test
260     public void receivePercentageTest() {
261         PercentageValue value = new PercentageValue(new BigDecimal(-100), new BigDecimal(100), new BigDecimal(10), null,
262                 null);
263         ChannelState c = spy(new ChannelState(config, channelUIDMock, value, channelStateUpdateListenerMock));
264         c.start(connectionMock, mock(ScheduledExecutorService.class), 100);
265
266         c.processMessage("state", "-100".getBytes()); // 0%
267         assertThat(value.getChannelState().toString(), is("0"));
268
269         c.processMessage("state", "100".getBytes()); // 100%
270         assertThat(value.getChannelState().toString(), is("100"));
271
272         c.processMessage("state", "0".getBytes()); // 50%
273         assertThat(value.getChannelState().toString(), is("50"));
274
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"));
279     }
280
281     @Test
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);
286
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"));
290
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"));
294
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"));
298
299         HSBType t = HSBType.fromRGB(12, 18, 231);
300
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"));
305     }
306
307     @Test
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);
312
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"));
316
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"));
320
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"));
324
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"));
328     }
329
330     @Test
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);
335
336         // incoming messages
337         c.processMessage("state", "ON".getBytes()); // Normal on state
338         assertThat(value.getChannelState().toString(), is("0,0,10"));
339
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));
343
344         c.processMessage("state", "10".getBytes()); // Brightness only
345         assertThat(value.getChannelState().toString(), is("0,0,10"));
346
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
350
351         // outgoing messages
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"),
356                 "100.0,0.60,0.30");
357     }
358
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));
368         }
369     }
370
371     @Test
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);
376
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"));
380     }
381
382     @Test
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);
387
388         ZonedDateTime zd = ZonedDateTime.now();
389         String datetime = zd.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
390
391         subject.processMessage("state", datetime.getBytes());
392
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));
397     }
398
399     @Test
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);
404
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"));
409     }
410 }