2 * Copyright (c) 2010-2023 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.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.mockito.ArgumentMatchers.*;
18 import static org.mockito.ArgumentMatchers.any;
19 import static org.mockito.Mockito.*;
21 import java.nio.charset.StandardCharsets;
22 import java.util.List;
23 import java.util.Objects;
25 import java.util.concurrent.CountDownLatch;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNull;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.junit.jupiter.api.AfterEach;
32 import org.junit.jupiter.api.BeforeEach;
33 import org.mockito.ArgumentMatchers;
34 import org.mockito.Mock;
35 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
36 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
37 import org.openhab.binding.mqtt.generic.values.Value;
38 import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
39 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
40 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
41 import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
42 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
43 import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatusInfo;
46 import org.openhab.core.thing.binding.ThingHandlerCallback;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.State;
51 * Abstract class for components tests.
53 * @author Anton Kharuzhy - Initial contribution
56 public abstract class AbstractComponentTests extends AbstractHomeAssistantTests {
57 private static final int SUBSCRIBE_TIMEOUT = 10000;
58 private static final int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
60 private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
61 private @NonNullByDefault({}) LatchThingHandler thingHandler;
64 public void setupThingHandler() {
65 final var config = haThing.getConfiguration();
67 config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
68 config.put(HandlerConfiguration.PROPERTY_TOPICS, getConfigTopics());
70 // Plumb thing status updates through
71 doAnswer(invocation -> {
72 ((Thing) invocation.getArgument(0)).setStatusInfo((ThingStatusInfo) invocation.getArgument(1));
74 }).when(callbackMock).statusUpdated(any(Thing.class), any(ThingStatusInfo.class));
76 when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
78 thingHandler = new LatchThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
79 SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
80 thingHandler.setConnection(bridgeConnection);
81 thingHandler.setCallback(callbackMock);
82 thingHandler = spy(thingHandler);
84 thingHandler.initialize();
88 public void disposeThingHandler() {
89 thingHandler.dispose();
93 * {@link org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents} will wait a config on specified
95 * Topics in config must be without prefix and suffix, they can be converted to full with method
96 * {@link #configTopicToMqtt(String)}
98 * @return config topics
100 protected abstract Set<String> getConfigTopics();
103 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
105 * @param mqttTopic mqtt topic with configuration
106 * @param json configuration payload in Json
107 * @return discovered component
109 protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
111 return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
115 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
117 * @param mqttTopic mqtt topic with configuration
118 * @param jsonPayload configuration payload in Json
119 * @return discovered component
121 protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
122 byte[] jsonPayload) {
123 var latch = thingHandler.createWaitForComponentDiscoveredLatch(1);
124 assertThat(publishMessage(mqttTopic, jsonPayload), is(true));
126 assert latch.await(1, TimeUnit.SECONDS);
127 } catch (InterruptedException e) {
128 assertThat(e.getMessage(), false);
130 return Objects.requireNonNull(thingHandler.getDiscoveredComponent());
134 * Assert channel topics, label and value class
136 * @param component component
137 * @param channelId channel
138 * @param stateTopic state topic or empty string
139 * @param commandTopic command topic or empty string
141 * @param valueClass value class
143 protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
144 String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass) {
145 var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
146 assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
150 * Assert channel topics, label and value class
152 * @param stateChannel channel
153 * @param stateTopic state topic or empty string
154 * @param commandTopic command topic or empty string
156 * @param valueClass value class
158 protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
159 String label, Class<? extends Value> valueClass) {
160 assertThat(stateChannel.getChannel().getLabel(), is(label));
161 assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
162 assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
163 assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
167 * Assert channel state
169 * @param component component
170 * @param channelId channel
171 * @param state expected state
173 @SuppressWarnings("null")
174 protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
175 String channelId, State state) {
176 assertThat(component.getChannel(channelId).getState().getCache().getChannelState(), is(state));
179 protected void spyOnChannelUpdates(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
181 // It's already thingHandler, but not the spy version
182 component.getChannel(channelId).getState().setChannelStateUpdateListener(thingHandler);
186 * Assert a channel triggers
188 protected void assertTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
189 String channelId, String trigger) {
190 verify(thingHandler).triggerChannel(eq(component.getChannel(channelId).getChannelUID()), eq(trigger));
194 * Assert that given payload was published exact-once on given topic.
196 * @param mqttTopic Mqtt topic
197 * @param payload payload
199 protected void assertPublished(String mqttTopic, String payload) {
200 verify(bridgeConnection).publish(eq(mqttTopic), ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)),
201 anyInt(), anyBoolean());
205 * Assert that given payload was published N times on given topic.
207 * @param mqttTopic Mqtt topic
208 * @param payload payload
209 * @param t payload must be published N times on given topic
211 protected void assertPublished(String mqttTopic, String payload, int t) {
212 verify(bridgeConnection, times(t)).publish(eq(mqttTopic),
213 ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
217 * Assert that given payload was not published on given topic.
219 * @param mqttTopic Mqtt topic
220 * @param payload payload
222 protected void assertNotPublished(String mqttTopic, String payload) {
223 verify(bridgeConnection, never()).publish(eq(mqttTopic),
224 ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
228 * Publish payload to all subscribers on specified topic.
230 * @param mqttTopic Mqtt topic
231 * @param payload payload
232 * @return true when at least one subscriber found
234 protected boolean publishMessage(String mqttTopic, String payload) {
235 return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
239 * Publish payload to all subscribers on specified topic.
241 * @param mqttTopic Mqtt topic
242 * @param payload payload
243 * @return true when at least one subscriber found
245 protected boolean publishMessage(String mqttTopic, byte[] payload) {
246 final var topicSubscribers = subscriptions.get(mqttTopic);
248 if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
249 topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
256 * Send command to a thing's channel
258 * @param component component
259 * @param channelId channel
260 * @param command command to send
262 protected void sendCommand(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
263 String channelId, Command command) {
264 var channel = Objects.requireNonNull(component.getChannel(channelId));
265 thingHandler.handleCommand(channel.getChannelUID(), command);
268 protected static class LatchThingHandler extends HomeAssistantThingHandler {
269 private @Nullable CountDownLatch latch;
270 private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
272 public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
273 TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
274 int attributeReceiveTimeout) {
275 super(thing, channelTypeProvider, transformationServiceProvider, subscribeTimeout, attributeReceiveTimeout);
279 public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
280 accept(List.of(component));
281 discoveredComponent = component;
287 public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
288 final var newLatch = new CountDownLatch(count);
293 public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
294 return discoveredComponent;