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.internal.handler;
15 import static org.hamcrest.CoreMatchers.is;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.assertThrows;
18 import static org.mockito.ArgumentMatchers.*;
19 import static org.mockito.Mockito.*;
20 import static org.openhab.binding.mqtt.generic.internal.handler.ThingChannelConstants.*;
22 import java.util.concurrent.CompletableFuture;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.junit.jupiter.api.BeforeEach;
27 import org.junit.jupiter.api.Test;
28 import org.junit.jupiter.api.extension.ExtendWith;
29 import org.mockito.Mock;
30 import org.mockito.junit.jupiter.MockitoExtension;
31 import org.mockito.junit.jupiter.MockitoSettings;
32 import org.mockito.quality.Strictness;
33 import org.openhab.binding.mqtt.generic.ChannelConfig;
34 import org.openhab.binding.mqtt.generic.ChannelConfigBuilder;
35 import org.openhab.binding.mqtt.generic.ChannelState;
36 import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
37 import org.openhab.binding.mqtt.generic.ThingHandlerHelper;
38 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
39 import org.openhab.binding.mqtt.generic.values.OnOffValue;
40 import org.openhab.binding.mqtt.generic.values.TextValue;
41 import org.openhab.binding.mqtt.generic.values.ValueFactory;
42 import org.openhab.binding.mqtt.handler.AbstractBrokerHandler;
43 import org.openhab.core.config.core.Configuration;
44 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
45 import org.openhab.core.library.types.StringType;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.ThingStatusInfo;
51 import org.openhab.core.thing.binding.ThingHandlerCallback;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.UnDefType;
56 * Tests cases for {@link GenericMQTTThingHandler}.
58 * @author David Graeff - Initial contribution
60 @ExtendWith(MockitoExtension.class)
61 @MockitoSettings(strictness = Strictness.LENIENT)
63 public class GenericThingHandlerTests {
65 private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
66 private @Mock @NonNullByDefault({}) Thing thingMock;
67 private @Mock @NonNullByDefault({}) AbstractBrokerHandler bridgeHandlerMock;
68 private @Mock @NonNullByDefault({}) MqttBrokerConnection connectionMock;
70 private @NonNullByDefault({}) GenericMQTTThingHandler thingHandler;
74 ThingStatusInfo thingStatus = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
76 // Mock the thing: We need the thingUID and the bridgeUID
77 when(thingMock.getUID()).thenReturn(TEST_GENERIC_THING);
78 when(thingMock.getChannels()).thenReturn(THING_CHANNEL_LIST);
79 when(thingMock.getStatusInfo()).thenReturn(thingStatus);
80 when(thingMock.getConfiguration()).thenReturn(new Configuration());
82 // Return the mocked connection object if the bridge handler is asked for it
83 when(bridgeHandlerMock.getConnectionAsync()).thenReturn(CompletableFuture.completedFuture(connectionMock));
85 CompletableFuture<@Nullable Void> voidFutureComplete = new CompletableFuture<>();
86 voidFutureComplete.complete(null);
87 doReturn(voidFutureComplete).when(connectionMock).unsubscribeAll();
88 doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).subscribe(any(), any());
89 doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).unsubscribe(any(), any());
90 doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).publish(any(), any(), anyInt(),
93 thingHandler = spy(new GenericMQTTThingHandler(thingMock, mock(MqttChannelStateDescriptionProvider.class),
94 mock(TransformationServiceProvider.class), 1500));
95 thingHandler.setCallback(callbackMock);
97 // Return the bridge handler if the thing handler asks for it
98 doReturn(bridgeHandlerMock).when(thingHandler).getBridgeHandler();
100 // The broker connection bridge is by default online
101 doReturn(thingStatus).when(thingHandler).getBridgeStatus();
105 public void initializeWithUnknownThingUID() {
106 ChannelConfig config = textConfiguration().as(ChannelConfig.class);
107 assertThrows(IllegalArgumentException.class,
108 () -> thingHandler.createChannelState(config, new ChannelUID(TEST_GENERIC_THING, "test"),
109 ValueFactory.createValueState(config, UNKNOWN_CHANNEL.getId())));
113 public void initialize() {
114 thingHandler.initialize();
115 verify(thingHandler).bridgeStatusChanged(any());
116 verify(thingHandler).start(any());
117 assertThat(thingHandler.getConnection(), is(connectionMock));
119 ChannelState channelConfig = thingHandler.channelStateByChannelUID.get(TEXT_CHANNEL_UID);
120 assertThat(channelConfig.getStateTopic(), is("test/state"));
121 assertThat(channelConfig.getCommandTopic(), is("test/command"));
123 verify(connectionMock).subscribe(eq(channelConfig.getStateTopic()), eq(channelConfig));
125 verify(callbackMock).statusUpdated(eq(thingMock), argThat(arg -> ThingStatus.ONLINE.equals(arg.getStatus())
126 && ThingStatusDetail.NONE.equals(arg.getStatusDetail())));
130 public void handleCommandRefresh() {
131 TextValue value = spy(new TextValue());
132 value.update(new StringType("DEMOVALUE"));
134 ChannelState channelConfig = mock(ChannelState.class);
135 doReturn(CompletableFuture.completedFuture(true)).when(channelConfig).start(any(), any(), anyInt());
136 doReturn(CompletableFuture.completedFuture(true)).when(channelConfig).stop();
137 doReturn(value).when(channelConfig).getCache();
138 doReturn(channelConfig).when(thingHandler).createChannelState(any(), any(), any());
139 thingHandler.initialize();
141 ThingHandlerHelper.setConnection(thingHandler, connectionMock);
143 thingHandler.handleCommand(TEXT_CHANNEL_UID, RefreshType.REFRESH);
144 verify(callbackMock).stateUpdated(eq(TEXT_CHANNEL_UID), argThat(arg -> "DEMOVALUE".equals(arg.toString())));
148 public void handleCommandUpdateString() {
149 TextValue value = spy(new TextValue());
150 ChannelState channelConfig = spy(
151 new ChannelState(ChannelConfigBuilder.create("stateTopic", "commandTopic").build(), TEXT_CHANNEL_UID,
152 value, thingHandler));
153 doReturn(channelConfig).when(thingHandler).createChannelState(any(), any(), any());
154 thingHandler.initialize();
155 ThingHandlerHelper.setConnection(thingHandler, connectionMock);
157 StringType updateValue = new StringType("UPDATE");
158 thingHandler.handleCommand(TEXT_CHANNEL_UID, updateValue);
159 verify(value).parseCommand(eq(updateValue));
160 // It didn't update the cached state
161 assertThat(value.getChannelState(), is(UnDefType.UNDEF));
165 public void handleCommandUpdateBoolean() {
166 OnOffValue value = spy(new OnOffValue("ON", "OFF"));
167 ChannelState channelConfig = spy(
168 new ChannelState(ChannelConfigBuilder.create("stateTopic", "commandTopic").build(), TEXT_CHANNEL_UID,
169 value, thingHandler));
170 doReturn(channelConfig).when(thingHandler).createChannelState(any(), any(), any());
171 thingHandler.initialize();
172 ThingHandlerHelper.setConnection(thingHandler, connectionMock);
174 StringType updateValue = new StringType("ON");
175 thingHandler.handleCommand(TEXT_CHANNEL_UID, updateValue);
177 verify(value).parseCommand(eq(updateValue));
181 public void processMessage() {
182 TextValue textValue = new TextValue();
183 ChannelState channelConfig = spy(
184 new ChannelState(ChannelConfigBuilder.create("test/state", "test/state/set").build(), TEXT_CHANNEL_UID,
185 textValue, thingHandler));
186 doReturn(channelConfig).when(thingHandler).createChannelState(any(), any(), any());
187 thingHandler.initialize();
188 byte[] payload = "UPDATE".getBytes();
189 // Test process message
190 channelConfig.processMessage("test/state", payload);
192 verify(callbackMock, atLeastOnce()).statusUpdated(eq(thingMock),
193 argThat(arg -> ThingStatus.ONLINE.equals(arg.getStatus())));
195 verify(callbackMock).stateUpdated(eq(TEXT_CHANNEL_UID), argThat(arg -> "UPDATE".equals(arg.toString())));
196 assertThat(textValue.getChannelState().toString(), is("UPDATE"));
200 public void handleBridgeStatusChange() {
201 Configuration config = new Configuration();
202 config.put("availabilityTopic", "test/LWT");
203 when(thingMock.getConfiguration()).thenReturn(config);
204 thingHandler.initialize();
206 .bridgeStatusChanged(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, null));
207 thingHandler.bridgeStatusChanged(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
208 verify(connectionMock, times(2)).subscribe(eq("test/LWT"), any());