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.anyBoolean;
19 import static org.mockito.ArgumentMatchers.anyInt;
20 import static org.mockito.ArgumentMatchers.eq;
21 import static org.mockito.Mockito.never;
22 import static org.mockito.Mockito.spy;
23 import static org.mockito.Mockito.times;
24 import static org.mockito.Mockito.verify;
25 import static org.mockito.Mockito.when;
27 import java.nio.charset.StandardCharsets;
28 import java.util.List;
30 import java.util.concurrent.CountDownLatch;
31 import java.util.concurrent.TimeUnit;
33 import org.eclipse.jdt.annotation.NonNull;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.hamcrest.CoreMatchers;
37 import org.junit.jupiter.api.AfterEach;
38 import org.junit.jupiter.api.BeforeEach;
39 import org.mockito.Mock;
40 import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
41 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
42 import org.openhab.binding.mqtt.generic.values.Value;
43 import org.openhab.binding.mqtt.homeassistant.internal.AbstractHomeAssistantTests;
44 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
45 import org.openhab.binding.mqtt.homeassistant.internal.HaID;
46 import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
47 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
48 import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.binding.ThingHandlerCallback;
51 import org.openhab.core.types.State;
54 * Abstract class for components tests.
56 * @author Anton Kharuzhy - Initial contribution
58 @SuppressWarnings({ "ConstantConditions" })
59 public abstract class AbstractComponentTests extends AbstractHomeAssistantTests {
60 private final static int SUBSCRIBE_TIMEOUT = 10000;
61 private final static int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
63 private @Mock ThingHandlerCallback callback;
64 private LatchThingHandler thingHandler;
67 public void setupThingHandler() {
68 final var config = haThing.getConfiguration();
70 config.put(HandlerConfiguration.PROPERTY_BASETOPIC, HandlerConfiguration.DEFAULT_BASETOPIC);
71 config.put(HandlerConfiguration.PROPERTY_TOPICS, getConfigTopics());
73 when(callback.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
75 thingHandler = new LatchThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
76 SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
77 thingHandler.setConnection(bridgeConnection);
78 thingHandler.setCallback(callback);
79 thingHandler = spy(thingHandler);
81 thingHandler.initialize();
85 public void disposeThingHandler() {
86 thingHandler.dispose();
90 * {@link org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents} will wait a config on specified
92 * Topics in config must be without prefix and suffix, they can be converted to full with method
93 * {@link #configTopicToMqtt(String)}
95 * @return config topics
97 protected abstract Set<String> getConfigTopics();
100 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
102 * @param mqttTopic mqtt topic with configuration
103 * @param json configuration payload in Json
104 * @return discovered component
106 protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
108 return discoverComponent(mqttTopic, json.getBytes(StandardCharsets.UTF_8));
112 * Process payload to discover and configure component. Topic should be added to {@link #getConfigTopics()}
114 * @param mqttTopic mqtt topic with configuration
115 * @param jsonPayload configuration payload in Json
116 * @return discovered component
118 protected AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoverComponent(String mqttTopic,
119 byte[] jsonPayload) {
120 var latch = thingHandler.createWaitForComponentDiscoveredLatch(1);
121 assertThat(publishMessage(mqttTopic, jsonPayload), is(true));
123 assert latch.await(1, TimeUnit.SECONDS);
124 } catch (InterruptedException e) {
125 assertThat(e.getMessage(), false);
127 var component = thingHandler.getDiscoveredComponent();
128 assertThat(component, CoreMatchers.notNullValue());
133 * Assert channel topics, label and value class
135 * @param component component
136 * @param channelId channel
137 * @param stateTopic state topic or empty string
138 * @param commandTopic command topic or empty string
140 * @param valueClass value class
142 protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
143 String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass) {
144 var stateChannel = component.getChannel(channelId);
145 assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
149 * Assert channel topics, label and value class
151 * @param stateChannel channel
152 * @param stateTopic state topic or empty string
153 * @param commandTopic command topic or empty string
155 * @param valueClass value class
157 protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
158 String label, Class<? extends Value> valueClass) {
159 assertThat(stateChannel.getChannel().getLabel(), is(label));
160 assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
161 assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
162 assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
166 * Assert channel state
168 * @param component component
169 * @param channelId channel
170 * @param state expected state
172 protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
173 String channelId, State state) {
174 assertThat(component.getChannel(channelId).getState().getCache().getChannelState(), is(state));
178 * Assert that given payload was published exact-once on given topic.
180 * @param mqttTopic Mqtt topic
181 * @param payload payload
183 protected void assertPublished(String mqttTopic, String payload) {
184 verify(bridgeConnection).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
189 * Assert that given payload was published N times on given topic.
191 * @param mqttTopic Mqtt topic
192 * @param payload payload
193 * @param t payload must be published N times on given topic
195 protected void assertPublished(String mqttTopic, String payload, int t) {
196 verify(bridgeConnection, times(t)).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)),
197 anyInt(), anyBoolean());
201 * Assert that given payload was not published on given topic.
203 * @param mqttTopic Mqtt topic
204 * @param payload payload
206 protected void assertNotPublished(String mqttTopic, String payload) {
207 verify(bridgeConnection, never()).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
212 * Publish payload to all subscribers on specified topic.
214 * @param mqttTopic Mqtt topic
215 * @param payload payload
216 * @return true when at least one subscriber found
218 protected boolean publishMessage(String mqttTopic, String payload) {
219 return publishMessage(mqttTopic, payload.getBytes(StandardCharsets.UTF_8));
223 * Publish payload to all subscribers on specified topic.
225 * @param mqttTopic Mqtt topic
226 * @param payload payload
227 * @return true when at least one subscriber found
229 protected boolean publishMessage(String mqttTopic, byte[] payload) {
230 final var topicSubscribers = subscriptions.get(mqttTopic);
232 if (topicSubscribers != null && !topicSubscribers.isEmpty()) {
233 topicSubscribers.forEach(mqttMessageSubscriber -> mqttMessageSubscriber.processMessage(mqttTopic, payload));
240 protected static class LatchThingHandler extends HomeAssistantThingHandler {
241 private @Nullable CountDownLatch latch;
242 private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
244 public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
245 TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
246 int attributeReceiveTimeout) {
247 super(thing, channelTypeProvider, transformationServiceProvider, subscribeTimeout, attributeReceiveTimeout);
250 public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<@NonNull ?> component) {
251 accept(List.of(component));
252 discoveredComponent = component;
258 public CountDownLatch createWaitForComponentDiscoveredLatch(int count) {
259 final var newLatch = new CountDownLatch(count);
264 public @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> getDiscoveredComponent() {
265 return discoveredComponent;