2 * Copyright (c) 2010-2022 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.homeassistant.internal.component;
15 import static org.hamcrest.CoreMatchers.instanceOf;
16 import static org.hamcrest.CoreMatchers.is;
17 import static org.hamcrest.MatcherAssert.assertThat;
18 import static org.mockito.ArgumentMatchers.any;
19 import static org.mockito.ArgumentMatchers.anyBoolean;
20 import static org.mockito.ArgumentMatchers.anyInt;
21 import static org.mockito.ArgumentMatchers.eq;
22 import static org.mockito.Mockito.doAnswer;
23 import static org.mockito.Mockito.never;
24 import static org.mockito.Mockito.spy;
25 import static org.mockito.Mockito.times;
26 import static org.mockito.Mockito.verify;
27 import static org.mockito.Mockito.when;
29 import java.nio.charset.StandardCharsets;
30 import java.util.List;
31 import java.util.Objects;
33 import java.util.concurrent.CountDownLatch;
34 import java.util.concurrent.TimeUnit;
36 import org.eclipse.jdt.annotation.NonNull;
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.junit.jupiter.api.AfterEach;
40 import org.junit.jupiter.api.BeforeEach;
41 import org.mockito.Mock;
42 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
43 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
44 import org.openhab.binding.mqtt.generic.values.Value;
45 import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
46 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
47 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
48 import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
49 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
50 import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatusInfo;
53 import org.openhab.core.thing.binding.ThingHandlerCallback;
54 import org.openhab.core.types.Command;
55 import org.openhab.core.types.State;
58 * Abstract class for components tests.
60 * @author Anton Kharuzhy - Initial contribution
62 @SuppressWarnings({ "ConstantConditions" })
64 public abstract class AbstractComponentTests extends AbstractHomeAssistantTests {
65 private static final int SUBSCRIBE_TIMEOUT = 10000;
66 private static final int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
68 private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
69 private @NonNullByDefault({}) LatchThingHandler thingHandler;
72 public void setupThingHandler() {
73 final var config = haThing.getConfiguration();
75 config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
76 config.put(HandlerConfiguration.PROPERTY_TOPICS, getConfigTopics());
78 // Plumb thing status updates through
79 doAnswer(invocation -> {
80 ((Thing) invocation.getArgument(0)).setStatusInfo((ThingStatusInfo) invocation.getArgument(1));
82 }).when(callbackMock).statusUpdated(any(Thing.class), any(ThingStatusInfo.class));
84 when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
86 thingHandler = new LatchThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
87 SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
88 thingHandler.setConnection(bridgeConnection);
89 thingHandler.setCallback(callbackMock);
90 thingHandler = spy(thingHandler);
92 thingHandler.initialize();
96 public void disposeThingHandler() {
97 thingHandler.dispose();
101 * {@link org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents} will wait a config on specified
103 * Topics in config must be without prefix and suffix, they can be converted to full with method
104 * {@link #configTopicToMqtt(String)}
106 * @return config topics
108 protected abstract Set<String> getConfigTopics();
111 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
113 * @param mqttTopic mqtt topic with configuration
114 * @param json configuration payload in Json
115 * @return discovered component
117 protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
119 return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
123 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
125 * @param mqttTopic mqtt topic with configuration
126 * @param jsonPayload configuration payload in Json
127 * @return discovered component
129 protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
130 byte[] jsonPayload) {
131 var latch = thingHandler.createWaitForComponentDiscoveredLatch(1);
132 assertThat(publishMessage(mqttTopic, jsonPayload), is(true));
134 assert latch.await(1, TimeUnit.SECONDS);
135 } catch (InterruptedException e) {
136 assertThat(e.getMessage(), false);
138 return Objects.requireNonNull(thingHandler.getDiscoveredComponent());
142 * Assert channel topics, label and value class
144 * @param component component
145 * @param channelId channel
146 * @param stateTopic state topic or empty string
147 * @param commandTopic command topic or empty string
149 * @param valueClass value class
151 protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
152 String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass) {
153 var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
154 assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
158 * Assert channel topics, label and value class
160 * @param stateChannel channel
161 * @param stateTopic state topic or empty string
162 * @param commandTopic command topic or empty string
164 * @param valueClass value class
166 protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
167 String label, Class<? extends Value> valueClass) {
168 assertThat(stateChannel.getChannel().getLabel(), is(label));
169 assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
170 assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
171 assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
175 * Assert channel state
177 * @param component component
178 * @param channelId channel
179 * @param state expected state
181 protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
182 String channelId, State state) {
183 assertThat(component.getChannel(channelId).getState().getCache().getChannelState(), is(state));
187 * Assert that given payload was published exact-once on given topic.
189 * @param mqttTopic Mqtt topic
190 * @param payload payload
192 protected void assertPublished(String mqttTopic, String payload) {
193 verify(bridgeConnection).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
198 * Assert that given payload was published N times on given topic.
200 * @param mqttTopic Mqtt topic
201 * @param payload payload
202 * @param t payload must be published N times on given topic
204 protected void assertPublished(String mqttTopic, String payload, int t) {
205 verify(bridgeConnection, times(t)).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)),
206 anyInt(), anyBoolean());
210 * Assert that given payload was not published on given topic.
212 * @param mqttTopic Mqtt topic
213 * @param payload payload
215 protected void assertNotPublished(String mqttTopic, String payload) {
216 verify(bridgeConnection, never()).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
221 * Publish payload to all subscribers on specified topic.
223 * @param mqttTopic Mqtt topic
224 * @param payload payload
225 * @return true when at least one subscriber found
227 protected boolean publishMessage(String mqttTopic, String payload) {
228 return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
232 * Publish payload to all subscribers on specified topic.
234 * @param mqttTopic Mqtt topic
235 * @param payload payload
236 * @return true when at least one subscriber found
238 protected boolean publishMessage(String mqttTopic, byte[] payload) {
239 final var topicSubscribers = subscriptions.get(mqttTopic);
241 if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
242 topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
249 * Send command to a thing's channel
251 * @param component component
252 * @param channelId channel
253 * @param command command to send
255 protected void sendCommand(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
256 String channelId, Command command) {
257 var channel = Objects.requireNonNull(component.getChannel(channelId));
258 thingHandler.handleCommand(channel.getChannelUID(), command);
261 protected static class LatchThingHandler extends HomeAssistantThingHandler {
262 private @Nullable CountDownLatch latch;
263 private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
265 public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
266 TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
267 int attributeReceiveTimeout) {
268 super(thing, channelTypeProvider, transformationServiceProvider, subscribeTimeout, attributeReceiveTimeout);
272 public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
273 accept(List.of(component));
274 discoveredComponent = component;
280 public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
281 final var newLatch = new CountDownLatch(count);
286 public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
287 return discoveredComponent;